Command Palette

Search for a command to run...

Hoe Hreflang toe te voegen in WordPress (Zonder een plug-in)

Hoe Hreflang toe te voegen in WordPress (Zonder een plug-in)

T
Toolz Team
|Aug 23, 2026|13 min lezen

Onderdeel van de collectie SEO-tools

Het meeste WordPress hreflang advies is & quot; installeer een meertalige plugin en het behandelt het." Dat is prima wanneer de plugin eigenaar is van uw vertalingen Het is het verkeerde antwoord in het geval dat ik het vaakst raak: een site met twee of drie taalversies gebouwd als afzonderlijke submappen, afzonderlijke installaties, of een set handmatig vertaalde pagina's, waar niemand een volledig vertaalkader en zijn databaseschema voor vier pagina's wil.

Ik onderhoud WordPress-eigenschappen in die vorm, inclusief documentatiesites waar de vertalingen met de hand zijn geschreven en onder leven /de/ en /es/. De hreflang er is veertig regels in een themabestand, het leest uit één array, en het is nooit afgedreven Deze gids is die code, plus de onderdelen die fout gaan wanneer een plugin hreflang uitzendt naast je thema.

Het zit onder de complete hreflang gids, naast de Volgende.js versie.

tl;dr: WordPress zendt niet uit zichzelf hreflang Haak wp_head, loop over een landkaart en print de volledige wederkerige set inclusief x-default op elke pagina in de groep Bouw URL's met home_url() of een per-locale oorsprong in plaats van hardcodering, bewaar elke pagina' s canonieke zelfreferentie, en controleer of geen enkele SEO-plugin al een concurrerend blok uitzendt Voor multi-site setups, get_sites() geeft u de kaart; voor een enkele site met vertaalde pagina's is een post-metaveld mapping-tegenhanger voldoende.

Voegt WordPress hreflang op zichzelf toe?

No. Core voert een canonieke link, een shortlink en verschillende feed- en REST-links uit wp_head, maar niets aan taal wisselt. get_locale() weet in welke taal de site zich bevindt en geen enkel deel van de kern verandert dat in een annotatie.

Plugins vullen het gat Polylang en WPML zenden hreflang uit voor de vertalingen die ze beheren, en de meeste SEO-plugins zullen het uitzenden als je een vertaalrelatie configureert die ze kunnen zien Als je al een van die gebruikt en het bestrijkt je hele site, gebruik het dan en sla dit artikel over De reden om met de hand te rollen is dat je vertalingen bestaan buiten wat de plug-in ook weet, en een plug-in die een gedeeltelijke set uitzendt is erger dan een plug-in die er geen uitzendt.

De minimale versie: één array, één haak

Voor een site waarvan de vertalingen een bekende, kleine set URL-voorvoegsels zijn:

<?php
/**
 * Hreflang annotations for a hand-translated multilingual site.
 * Drop in the theme's functions.php or a small mu-plugin.
 */
function mysite_hreflang_map(): array {
    return [
        'en-US'     => 'https://example.com',
        'de-DE'     => 'https://example.com/de',
        'es-ES'     => 'https://example.com/es',
        'x-default' => 'https://example.com',
    ];
}

function mysite_print_hreflang(): void {
    $path = mysite_current_path(); // e.g. '/pricing/'
    foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
        printf(
            '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
            esc_attr( $hreflang ),
            esc_url( untrailingslashit( $origin ) . $path )
        );
    }
}
add_action( 'wp_head', 'mysite_print_hreflang', 1 );

Drie dingen die dit goed doet, doen handgeschreven blokken meestal niet.

Het drukt de volledige set op elke pagina af, inclusief de pagina&#39; eigen taal Wederkerigheid is structureel tevreden: er is geen tak die een ingang zou kunnen weglaten, omdat er helemaal geen tak is.

x-default is een rij in dezelfde array, dus het wordt uitgezonden door dezelfde lus en kan niet worden vergeten op één sjabloon.

URL's worden ontsnapt. esc_url op een href in wp_head is niet optioneel en hreflang-waarden gaan door esc_attr om dezelfde reden.

Het uitwerken van het huidige pad

Het pad is het deel dat zorg nodig heeft, omdat het het pad moet zijn tot op de taal, niet de volledige aanvraag-URI.

function mysite_current_path(): string {
    $home = wp_parse_url( home_url(), PHP_URL_PATH ) ?: '';
    $req  = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH ) ?: '/';

    // Strip the language prefix this install is served under.
    if ( $home && str_starts_with( $req, $home ) ) {
        $req = substr( $req, strlen( $home ) );
    }
    return user_trailingslashit( '/' . ltrim( $req, '/' ) );
}

Als uw Duitse installatie leeft op example.com/de en de bezoeker is aan example.com/de/preise/, dit keert terug /preise/, die vervolgens wordt voorafgegaan door elke locale&#39;s oorsprong. De hantering van de achterliggende schuine streep is belangrijker dan het lijkt: /preise en /preise/ zijn verschillende URL's, en het aanwijzen van een annotatie op de niet-canonieke vorm is de mismatch tussen achterwaartse en schuine streep dat levert return-tag fouten op een set die er anders perfect uitziet. user_trailingslashit respecteert de site&#39; s permalink instelling, dus het komt overeen met wat WordPress ook serveert.

De aanname hierbij is dat vertaalde URL's structureel parallel zijn: /pricing/ kaarten naar /de/pricing/. Als uw Duitse slugs ook vertaald is, /de/preise/, deze functie is niet genoeg en je hebt een expliciete mapping nodig, wat de volgende sectie is.

Wanneer slugs verschilt: een post-meta mapping

Vertaalde slugs zijn beter voor gebruikers en voor rangschikking, en ze betekenen dat het pad niet kan worden afgeleid. Bewaar in plaats daarvan de tegenhangers:

/**
 * Meta key holds a JSON map: {"de-DE": 123, "es-ES": 456} of post IDs,
 * or absolute URLs for external installs.
 */
function mysite_alternates_for( int $post_id ): array {
    $raw = get_post_meta( $post_id, '_hreflang_alternates', true );
    $map = $raw ? json_decode( $raw, true ) : [];
    if ( ! is_array( $map ) ) {
        return [];
    }

    $out = [];
    foreach ( $map as $hreflang => $target ) {
        $url = is_numeric( $target ) ? get_permalink( (int) $target ) : $target;
        if ( $url ) {
            $out[ $hreflang ] = $url;
        }
    }
    return $out;
}

Dan de wp_head callback voegt de huidige post&#39; eigen invoer in die kaart voordat deze wordt afgedrukt, dus de set is compleet en zelfreferentieel:

function mysite_print_hreflang_mapped(): void {
    if ( ! is_singular() ) {
        return;
    }
    $post_id = get_queried_object_id();
    $alts    = mysite_alternates_for( $post_id );
    if ( count( $alts ) < 1 ) {
        return; // Nothing to declare: a one-entry cluster is not a cluster.
    }

    $alts[ mysite_hreflang_for_locale( get_locale() ) ] = get_permalink( $post_id );
    $alts['x-default'] = $alts['en-US'] ?? get_permalink( $post_id );

    foreach ( $alts as $hreflang => $url ) {
        printf(
            '<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
            esc_attr( $hreflang ),
            esc_url( $url )
        );
    }
}

De onderhoudskosten zijn dat de meta aan beide kanten moet worden geschreven Een admin UI met een tegenhanger picker is de eerlijke oplossing; een WP-CLI commando dat de kaart van een CSV vult is de snelle Wat je niet moet doen is het vullen op slechts één kant, omdat een eenrichtingsverklaring is precies de ontbrekende return tag failure.

Multisite: leid de kaart af van het netwerk

Als elke taal een site is in een multisite netwerk, bestaat de kaart al

function mysite_network_hreflang_map(): array {
    $map = [];
    foreach ( get_sites( [ 'number' => 50 ] ) as $site ) {
        switch_to_blog( (int) $site->blog_id );
        $map[ mysite_hreflang_for_locale( get_locale() ) ] = home_url();
        restore_current_blog();
    }
    return $map;
}

Cache het resultaat in een transient; switch_to_blog in een wp_head hook op elk verzoek is een echte kostenpost op een netwerk van elke omvang En wees voorzichtig met get_locale() onder switch_to_blog: het weerspiegelt de geschakelde site alleen als de site&#39;s WPLANG optie is ingesteld, wat het gebruikelijke geval is, maar niet gegarandeerd op netwerken die via een constante zijn geconfigureerd.

Welke aanpak past bij uw site?

instelling toegang Inspanning
Parallel slugs, weinig locaties Statische array plus afgeleid pad Laagste; één functie
Vertaald slugs Post-meta tegenhangerkaart Matig; heeft een bewerkingsworkflow nodig
Multisite, één site per taal Afleiden van get_sites(), in de cache opgeslagen Lage code, heeft cachediscipline nodig
Polylang of WPML beheert vertalingen Gebruik de plugin&#39;s-uitvoer Controleer of het x-standaard uitzendt
SEO plugin met handmatige vertaalvelden Gebruik het en controleer vervolgens het weergegeven hoofd Let op dubbele blokken

In plaats daarvan hreflang in de sitemap plaatsen

Als je paginakoppen al vol zijn, of je hebt genoeg locales dat het blok langer is dan de rest van het hoofd, dan is de sitemap het alternatief WordPress 5.5 en later verzenden kern sitemaps, en ze zijn filtreerbaar, hoewel de filters niet zijn ontworpen met alternatieven in gedachten en de naamruimtedeclaratie erop <urlset> is niet iets dat je door de kern schoon kunt aanraken.

De pragmatische route is een op zichzelf staande sitemap die wordt geserveerd vanuit een herschrijfregel in plaats van een gevecht met wp_sitemapspiepsel

add_action( 'init', function () {
    add_rewrite_rule( '^sitemap-i18n\.xml$', 'index.php?mysite_i18n_sitemap=1', 'top' );
} );

add_filter( 'query_vars', fn( $vars ) => array_merge( $vars, [ 'mysite_i18n_sitemap' ] ) );

add_action( 'template_redirect', function () {
    if ( ! get_query_var( 'mysite_i18n_sitemap' ) ) {
        return;
    }
    header( 'Content-Type: application/xml; charset=UTF-8' );

    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ';
    echo 'xmlns:xhtml="http://www.w3.org/1999/xhtml">' . "\n";

    foreach ( mysite_translated_paths() as $path ) {
        echo "  <url>\n    <loc>" . esc_url( home_url( $path ) ) . "</loc>\n";
        foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
            printf(
                '    <xhtml:link rel="alternate" hreflang="%s" href="%s"/>' . "\n",
                esc_attr( $hreflang ),
                esc_url( untrailingslashit( $origin ) . $path )
            );
        }
        echo "  </url>\n";
    }
    echo '</urlset>';
    exit;
} );

Twee dingen om goed te komen De xmlns:xhtml verklaring op <urlset> is vereist; zonder dit worden de alternatieve vermeldingen genegeerd en valideert het bestand nog steeds als een sitemap, wat weer het stille foutpatroon is. En spoel de regels één keer door na het toevoegen van de regel, bij activering in plaats van bij elk verzoek.

Nadat u dit heeft gedaan, verwijdert u de hoofduitvoer. Beide uitzenden is implementatiemethoden mengen, wat je twee bronnen geeft die afdrijven.

Het duplicaat-blok probleem

Dit is de WordPress-specifieke storing en degene die de moeite waard is om te controleren voordat u code schrijft Verschillende plug-ins zenden hreflang uit, en er kunnen er meer dan één tegelijk actief zijn Een SEO-plug-in geconfigureerd met vertaalrelaties, een meertalige plug-in en uw nieuwe themafunctie worden allemaal met plezier afgedrukt wp_head, en het resultaat is twee of drie blokken met verschillende inhoud en mogelijk twee x-default entries.

Google&#39; s behandeling van tegenstrijdige annotaties binnen één pagina is om ze te wantrouwen Dus een site die geen hreflang had en nu drie blokken heeft, kan slechter af zijn dan voorheen.

Controleer de weergegeven uitvoer in plaats van de plug-ininstellingen:

curl -s https://example.com/de/preise/ | grep 'rel="alternate"' | grep hreflang

Tel de regels en tel de verschillende codes Eén invoer per landinstelling plus één x-default, en niets herhaald Als een plugin een set uitzendt die u niet kunt wegconfigureren, is het gebruikelijke ontsnappingsluik om de actie ervan te verwijderen wp_head in plaats van de jouwe erbovenop toe te voegen:

// Example shape: find the real hook and callback with `has_action`.
add_action( 'init', function () {
    remove_action( 'wp_head', 'some_plugin_hreflang_output', 10 );
} );

Canonieke tags, en deze niet breken

WordPress zendt rel="canonical" voor unieke weergaven door rel_canonical(), en het wijst naar de post&#39;s eigen permalink, wat je wilt De meeste SEO plugins vervangen het door hun eigen, ook standaard zelfreferentieel.

De regel van hreflang vs canoniek is ongewijzigd van toepassing: elke pagina canonicaliseert voor zichzelf, en hreflang beschrijft de relaties tussen die zelfcanonieke pagina's De manier waarop WordPress het opbreekt is een plugin-instelling die vertaalde berichten canonicaliseert naar een & quot; primary&quot; taalversie, die leest als een verstandige deduplicatieoptie en stilletjes uw vertalingen uit de index verwijdert Als een instelling biedt om canonicaliseren in verschillende talen, laat het dan uit.

Het resultaat verifiëren

Haal twee pagina's van broers en zussen op en vergelijk hun annotatieblokken Ze moeten identiek zijn, waarbij alleen de canonieke verschillen Dan controleer je of elke URL die je hebt genoemd 200 retourneert zonder omleiding, omdat gepensioneerde locales en gewijzigde permalinks zijn hoe een juiste set in een gebroken rots.

Plak de set in de gratis hreflang generator om de codes te valideren: het controleert elke waarde aan de hand van de ISO-vorm, vlaggen niet-herkende subtags zoals en-UK, vangt duplicaten op, wijst relatieve URL's af en waarschuwt wanneer de set geen terugval heeft Het draait volledig in de browser, wat van belang is wanneer de URL's een clientsite beschrijven die nog niet openbaar is Gebruik vervolgens Search Console&#39; s International Targeting rapport voor de lopende controle, en de hreflang gereedschap gids voor de workflow eromheen.

Veelgestelde vragen

Voegt WordPress automatisch hreflang-tags toe?

No. Core zendt een canonieke link en verschillende hoofdlinks uit maar niets over taalwisselingen Hreflang komt uit een plugin of uit je eigen wp_head output.

Hoe voeg ik hreflang toe in WordPress zonder plugin?

Hook wp_head en print een <link rel="alternate" hreflang> regel voor elke landinstelling in een kaart die u beheert, inclusief een x-default invoer, op elke pagina in de groep Ontsnap aan de waarden met esc_attr en esc_url.

Waar gaan hreflang-tags heen in een WordPress-thema?

In een functie die is gekoppeld aan wp_head, idealiter in een kleine mu-plug-in in plaats van in het thema, dus het wisselen van thema laat je annotaties niet stilletjes vallen.

Wat als mijn vertaalde pagina's verschillende slugs hebben?

Niets afleiden Bewaar de tegenhanger post ID's of URL's in post meta en lees de kaart op render tijd Populeer het aan beide kanten, of je maakt een ontbrekende retour tag.

Handelen Polylang en WPML hreflang?

Ja, voor de vertalingen die zij beheren Verifieer de uitvoer omvat x-default en dat geen enkele tweede plugin een concurrerend blok uitzendt.

Waarom heb ik twee hreflang blokken op één pagina?

Twee dingen zijn afdrukken naar wp_head: meestal een SEO-plugin plus een meertalige plugin, of een plugin plus een eigen functie Tegenstrijdige annotaties op één pagina worden gewantrouwd, dus verwijder er één in plaats van gelaagdheid.

Moet elke WordPress-taalversie zijn eigen canonieke versie hebben?

Ja, zelfreferentieel Gebruik geen plugin-instelling die vertaalde berichten canonicaliseert naar een primaire taal, omdat deze dan uit de index worden verwijderd en de hreflang-set wordt geannuleerd.

Hoe voeg ik hreflang toe via een WordPress multisite-netwerk?

Loop get_sites(), schakel naar elke blog om de landinstelling te lezen en home_url(), en cache de resulterende kaart in een transient Uitvoeren switch_to_blog op elk verzoek in wp_head is duur op een groot netwerk.


Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!