Bestellen
Hosting
Domeinnaam
Ondersteuning
Ontdek

Op deze pagina

Blog / SEO · · ~22 min lezen

WordPress SEO-plugin vervangen door Schema.org markup

WordPress SEO-plugin vervangen door Schema.org markup: minder plugin-overhead, JSON-LD zelf beheren. Stappenplan, code en veelgemaakte fouten.

Geschreven door: Pablo Cleij Pablo Cleij
Deel dit artikel

Samenvatting: SEO-plugin vervangen door Schema.org markup

Google noemt JSON-LD zelf het aanbevolen formaat voor structured data[1]. Handmatige JSON-LD via functions.php weegt doorgaans minder dan 1KB[2], tegenover bijna 15MB aan pluginbestanden voor Yoast SEO alleen al[3]. Dit artikel laat zien wanneer die wissel loont, hoe je de code veilig plaatst via een child theme, en hoe je dubbele schema met een draaiende SEO-plugin voorkomt.

Yoast SEO draait op 30,4% van alle Nederlandse WordPressKeurigOnline zegtWordPressWordPress is een open-source contentmanagementsysteem (CMS) geschreven in PHP, waarmee je eenvoudig websites en blogs kunt bouwen en beheren.-sites[4], meestal voor meta titles, sitemaps en structured data tegelijk. Voor dat laatste onderdeel alleen sleep je een compleet pluginraamwerk mee: instellingenschermen, databaseKeurigOnline zegtDatabaseEen database is een georganiseerde verzameling gegevens die elektronisch wordt opgeslagen en beheerd.-opslag en extra PHPKeurigOnline zegtPHPPHP (Hypertext Preprocessor) is een populaire open-source scripttaal die speciaal is ontworpen voor webontwikkeling en server-side programmering.-verwerking bij elke paginaload. Staat de plugin alleen nog aan voor de schema markup? Dan neem je die taak net zo goed zelf over, met een paar regels code.

Hieronder lees je wat Schema.org markup precies doet, wanneer handmatig beheer verstandig is, en hoe je het stap voor stap bouwt zonder dat je bij de volgende thema-update alles kwijtraakt.

Een hand tilt een zwaar tandwiel weg terwijl een andere hand een kleine stempel in een boek drukt

Wat is Schema.org markup (en waarom JSON-LD)?

Schema.org is een gedeeld vocabulaire waarmee je aan zoekmachines vertelt wát er op een pagina staat, niet alleen dát er tekst staat. Een blogartikel wordt een Article, je bedrijf een Organization, een navigatiepad een BreadcrumbList. Google leest die markup en zet ze soms om in rich results: sterrenbeoordelingen, broodkruimels in de zoekresultaten, of een uitklapbare FAQ.

Er bestaan drie manieren om die markup te schrijven: JSONKeurigOnline zegtJSONJSON is een lichtgewicht dataformaat voor het uitwisselen van gegevens tussen systemen.-LD, Microdata en RDFa. Google is daar expliciet over: JSON-LD is het aanbevolen formaat, "omdat het voor de meeste website-eigenaren het makkelijkst te implementeren en onderhouden is"[1], een aanbeveling die in de Search Central-changelog nog eens is bevestigd[5]. Anders dan Microdata en RDFa, die je middenin je zichtbare HTMLKeurigOnline zegtHTMLHTML is de programmeertaal voor het maken van webpagina’s. moet verweven, staat JSON-LD los in een eigen <script>-blok[6]. Je voegt het toe zonder je template-HTML aan te raken, en zonder dat een plugin ertussen hoeft te zitten.

Een SEO-plugin genereert die JSON-LD dynamisch bij elke paginaload, verstopt in een instellingenscherm dat je zelden opent. Jij kunt exact hetzelfde script zelf schrijven, met een fractie van de overhead.

Waarom zou je een SEO-plugin hiervoor vervangen?

Yoast SEO is wereldwijd goed voor 69,5% van alle SEO-plugins en draait op 45,6% van alle WordPress-sites[7]; Rank Math volgt met 13,1% wereldwijd marktaandeel[8]. Beide zijn goede plugins, maar ze wegen wat.

Een onafhankelijke benchmark op GeneratePress met PHP 8.2 liet zien dat Yoast Free ongeveer 33KB paginagewicht en 148ms Total Blocking Time toevoegt bovenop een kale baseline van 90ms; Rank Math Free bleef bij ongeveer 12KB en 92ms[9]. WP Rocket's eigen cijfers laten een vergelijkbaar verschil zien in bestandsgrootte: 11,8MB voor Rank Math tegenover 32,4MB voor Yoast[9].

Een aparte plugin-vergelijking zet dat verschil nog scherper neer: een minimale schema-plugin van 186KB haalt gemiddeld 132ms TTFBKeurigOnline zegtTTFBTTFB (Time to First Byte) meet de tijd tussen een HTTP-verzoek en de eerste byte van het serverantwoord., terwijl Yoast (bijna 15MB, 1.604 bestanden) daar 81 keer groter voor is en All in One SEO zelfs 100 keer[3]. AIOSEO kan bovendien tot 15 extra databasequeries per paginaload genereren, goed voor 200 tot 400ms extra TTFB op shared hostingKeurigOnline zegtShared hostingShared hosting is een hostingvorm waarbij meerdere websites dezelfde serverresources delen., omdat de JSON-LD-schema bij elke request opnieuw wordt opgebouwd in plaats van gecachet[10]. Handmatige JSON-LD via functions.php weegt daarentegen minder dan 1KB en heeft vrijwel geen meetbare invloed op de laadtijd[2].

Plugin-bloat is een bekende oorzaak van trage WordPress-sites, zoals we eerder beschreven in 5 redenen waarom je WordPress site zo langzaam is: elke module die je uitschakelt, telt mee. Zie ook hoe plugins je Core Web Vitals beïnvloeden voor het bredere plaatje.

Wanneer is handmatig wél of niet de juiste keuze?

Handmatige JSONKeurigOnline zegtJSONJSON is een lichtgewicht dataformaat voor het uitwisselen van gegevens tussen systemen.-LD werkt het best bij een sitestructuur die niet elke week verandert: een bedrijfssite met een vaste set paginatypes, of een blog met artikelen die allemaal dezelfde velden nodig hebben (titel, auteur, publicatiedatum). Je schrijft de code één keer, en WordPress vult de rest automatisch in per pagina.

Bij een webshop met WooCommerceKeurigOnline zegtWooCommerceWooCommerce is de gratis e-commerce plugin voor WordPress. Bouw een professionele webshop met producten, betalingen en verzending — zonder technische kennis.-producten, wisselende voorraad en prijzen, of een team van niet-technische beheerders die zelf content toevoegen, blijft een plugin de praktischere keuze. Rank Math en Yoast onderhouden hun schema-module actief, testen tegen wijzigingen bij Google, en bieden een interface waarin een collega zonder codekennis een FAQ-blok kan toevoegen. Wil je liever bij een plugin blijven, dan lees je in onze complete gids voor Yoast SEO hoe je die instelt.

De vuistregel: hoe voorspelbaarder je contentstructuur, hoe meer een handmatige aanpak oplevert. Hoe meer variatie en niet-technisch beheer, hoe meer een plugin zijn overhead terugverdient.

Voorbereiding: child theme en waar je code plaatst

Voeg je code rechtstreeks toe aan het functions.php-bestand van je actieve thema, dan verdwijnt die code zodra het thema een update krijgt: WordPress overschrijft themabestanden volledig bij elke update. Zonder child theme ben je dus na de eerstvolgende thema-update je schema markup kwijt, zonder waarschuwing[2].

Je hebt twee veilige opties:

  • Child theme: een submap met een eigen functions.php die de code van het hoofdthema aanvult. De standaardkeuze als je verder ook kleine visuele aanpassingen doet.
  • Mu-plugin (must-use plugin): een bestand in wp-content/mu-plugins/. Dit werkt volledig thema-onafhankelijk, blijft actief als je later van thema wisselt, en kan niet per ongeluk worden uitgeschakeld.

Twijfel je of je ooit van thema wisselt? Kies de mu-plugin: je schema markup hoort bij je site, niet bij je vormgeving. Wil je dieper in de WordPress-architectuur duiken, bijvoorbeeld hoe hooks zich verhouden tot moderne uitbreidingsmechanismen, lees dan onze uitleg over de WordPress Abilities API.

Stap voor stap: JSON-LD toevoegen via functions.php

Eén functie die op wp_head haakt en één <script>-blok print, werkt voor één schema-type. Zodra je Organization, WebSite, Article, breadcrumbs én FAQ tegelijk wilt uitleveren, eindig je met vijf losse scripts in de <head>, die je stuk voor stuk moet onderhouden en die elkaar makkelijk in de weg zitten. Slimmer: bouw één centrale @graph, waarin alle schema-nodes van de pagina via een @id naar elkaar verwijzen, en laat elk type zichzelf daarin registreren via een eigen WordPress-filter. Zo voeg je later een nieuw type toe zonder ooit de kernfunctie aan te raken.

Eerst de motor: één functie die alle geregistreerde schema-nodes verzamelt via het filter ko_schema_graph en ze in één JSON-LD-blok uitprint.

// In functions.php van je child theme, of in een mu-plugin

/**
 * Verwijdert lege/null-velden uit een schema-node, zodat je nooit
 * een ongeldig "image": null of "sku": "" in de output krijgt.
 */
function ko_strip_empty( array $node ) {
    return array_filter(
        $node,
        static function ( $value ) {
            return null !== $value && '' !== $value && array() !== $value;
        }
    );
}

/**
 * De motor: verzamelt alle geregistreerde schema-nodes en print ze
 * als één @graph. Andere functies breiden dit uit via het
 * 'ko_schema_graph' filter, zonder deze functie te wijzigen.
 */
function ko_render_schema_graph() {
    $graph = apply_filters( 'ko_schema_graph', array() );
    $graph = array_values( array_filter( $graph ) );

    if ( empty( $graph ) ) {
        return;
    }

    $document = array(
        '@context' => 'https://schema.org',
        '@graph'   => $graph,
    );

    echo '<script type="application/ld+json">' .
        wp_json_encode( $document, JSON_UNESCAPED_SLASHES ) .
        '</script>' . "\n";
}
add_action( 'wp_head', 'ko_render_schema_graph' );

Nu registreer je elk schema-type als een losse functie: zelf inhaken op het filter, een eigen paginaconditie bewaken en dynamische velden teruggeven via wp_json_encode() in plaats van hardcoded tekst[11]. De Organization-node, sitebreed op de homepage:

add_filter( 'ko_schema_graph', 'ko_schema_organization' );
function ko_schema_organization( $graph ) {
    if ( ! is_front_page() ) {
        return $graph;
    }

    $graph[] = ko_strip_empty( array(
        '@type' => 'Organization',
        '@id'   => home_url( '/#organization' ),
        'name'  => get_bloginfo( 'name' ),
        'url'   => home_url( '/' ),
        'logo'  => get_site_icon_url(),
    ) );

    return $graph;
}

Elke volgende builder in dit artikel volgt exact dit patroon: add_filter( 'ko_schema_graph', ... ), een conditie die bepaalt of het type op déze pagina thuishoort, en velden die met WordPress- of WooCommerce-functies worden ingevuld in plaats van met tekst die je per pagina zou moeten aanpassen. Voeg je later een zesde of zevende type toe, dan plak je alleen een nieuwe functie onder de rest, de motor blijft ongewijzigd.

De schema-types die de meeste WordPress-sites nodig hebben

Je hoeft niet elk Schema.org-type te implementeren, maar met alleen Organization en Article mis je context waar Google om vraagt: een zoekbalk in de resultaten, een broodkruimelpad, of een productprijs. De vijf builders hieronder dekken samen wat de meeste bedrijfssites, blogs en webshops echt gebruiken, elk als losse functie op hetzelfde ko_schema_graph-filter uit de vorige stap[11]. Vul verplichte velden altijd echt in: een Article heeft minimaal headline, image, datePublished en author nodig, een Product heeft name, image en offers nodig[12]. Niet elke pagina heeft schema nodig: een privacyverklaring of 404-pagina voegt niets toe aan een rich result.

WebSite, met zoekactie op de homepage

Heeft je site een zoekfunctie, dan kan Google die als sitelinks-zoekvak tonen. De conditie is identiek aan die van Organization, maar het is een apart type met een eigen @id.

add_filter( 'ko_schema_graph', 'ko_schema_website' );
function ko_schema_website( $graph ) {
    if ( ! is_front_page() ) {
        return $graph;
    }

    $graph[] = array(
        '@type'           => 'WebSite',
        '@id'             => home_url( '/#website' ),
        'name'            => get_bloginfo( 'name' ),
        'url'             => home_url( '/' ),
        'potentialAction' => array(
            '@type'       => 'SearchAction',
            'target'      => home_url( '/?s={search_term_string}' ),
            'query-input' => 'required name=search_term_string',
        ),
    );

    return $graph;
}

BreadcrumbList, dynamisch per pagina opgebouwd

Een broodkruimelpad hardcoden per pagina is precies het soort onderhoud dat je met handmatige schema wilt vermijden. Deze builder leest in plaats daarvan de categorie van een artikel of de paginaboom van een pagina uit, en bouwt het pad zelf op, voor elke pagina anders, zonder dat je ooit een array met de hand aanpast.

add_filter( 'ko_schema_graph', 'ko_schema_breadcrumbs' );
function ko_schema_breadcrumbs( $graph ) {
    if ( is_front_page() ) {
        return $graph;
    }

    $items   = array();
    $items[] = array( __( 'Home', 'ko' ), home_url( '/' ) );

    if ( is_single() ) {
        $cats = get_the_category();
        if ( ! empty( $cats ) ) {
            $items[] = array( $cats[0]->name, get_category_link( $cats[0]->term_id ) );
        }
        $items[] = array( get_the_title(), get_permalink() );
    } elseif ( is_page() ) {
        foreach ( array_reverse( get_post_ancestors( get_the_ID() ) ) as $ancestor_id ) {
            $items[] = array( get_the_title( $ancestor_id ), get_permalink( $ancestor_id ) );
        }
        $items[] = array( get_the_title(), get_permalink() );
    } else {
        return $graph; // archieven e.d. hebben geen vast pad, sla over
    }

    $list_items = array();
    foreach ( $items as $position => $item ) {
        $list_items[] = array(
            '@type'    => 'ListItem',
            'position' => $position + 1,
            'name'     => $item[0],
            'item'     => $item[1],
        );
    }

    $graph[] = array(
        '@type'           => 'BreadcrumbList',
        'itemListElement' => $list_items,
    );

    return $graph;
}

Article/BlogPosting, gekoppeld aan je Organization-node

In plaats van een losse publisher met alle Organization-velden opnieuw te schrijven, verwijst deze builder via @id terug naar de Organization-node van de homepage. Google leest de relatie via die verwijzing, jij hoeft niets te dupliceren.

add_filter( 'ko_schema_graph', 'ko_schema_article' );
function ko_schema_article( $graph ) {
    if ( ! is_single() ) {
        return $graph;
    }

    $image_id = get_post_thumbnail_id();

    $graph[] = ko_strip_empty( array(
        '@type'            => 'BlogPosting',
        'headline'         => get_the_title(),
        'image'            => $image_id ? wp_get_attachment_image_url( $image_id, 'full' ) : null,
        'author'           => array(
            '@type' => 'Person',
            'name'  => get_the_author(),
        ),
        'publisher'        => array( '@id' => home_url( '/#organization' ) ),
        'datePublished'    => get_the_date( 'c' ),
        'dateModified'     => get_the_modified_date( 'c' ),
        'mainEntityOfPage' => get_permalink(),
    ) );

    return $graph;
}

Product, met live prijs en voorraad uit WooCommerce

Draait je site op WooCommerce, dan haalt deze builder prijs, valuta, voorraadstatus en beoordelingen rechtstreeks uit het productobject, dus altijd actueel, nooit een vergeten prijswijziging in een losse schema-array. aggregateRating wordt alleen toegevoegd als er daadwerkelijk beoordelingen zijn: een reviewCount van 0 meesturen levert ongeldige schema op[12].

add_filter( 'ko_schema_graph', 'ko_schema_product' );
function ko_schema_product( $graph ) {
    if ( ! function_exists( 'is_product' ) || ! is_product() ) {
        return $graph; // WooCommerce niet actief, of geen productpagina
    }

    global $product;
    if ( ! $product instanceof WC_Product ) {
        return $graph;
    }

    $node = ko_strip_empty( array(
        '@type'  => 'Product',
        'name'   => $product->get_name(),
        'image'  => wp_get_attachment_image_url( $product->get_image_id(), 'full' ),
        'sku'    => $product->get_sku(),
        'offers' => array(
            '@type'         => 'Offer',
            'priceCurrency' => get_woocommerce_currency(),
            'price'         => $product->get_price(),
            'availability'  => $product->is_in_stock()
                ? 'https://schema.org/InStock'
                : 'https://schema.org/OutOfStock',
            'url'           => get_permalink( $product->get_id() ),
        ),
    ) );

    if ( $product->get_review_count() > 0 ) {
        $node['aggregateRating'] = array(
            '@type'       => 'AggregateRating',
            'ratingValue' => $product->get_average_rating(),
            'reviewCount' => $product->get_review_count(),
        );
    }

    $graph[] = $node;
    return $graph;
}

FAQPage, automatisch uit je artikel-content

De meeste handleidingen laten je de FAQ-schema handmatig overtypen naast de zichtbare vraag en antwoord op de pagina, met als gevolg dat de twee na een tekstwijziging uit elkaar gaan lopen (precies de mismatch waar Google op afrekent, zie hierna). Slimmer: lees de FAQ-items rechtstreeks uit de gerenderde content, met DOMDocument, zodat schema en zichtbare tekst per definitie gelijk zijn en je nooit iets dubbel onderhoudt. Deze builder zoekt naar dezelfde .ai-faq-item-structuur die dit artikel voor zijn eigen FAQ-sectie gebruikt.

add_filter( 'ko_schema_graph', 'ko_schema_faq_from_content' );
function ko_schema_faq_from_content( $graph ) {
    if ( ! is_single() ) {
        return $graph;
    }

    $content = get_the_content();
    if ( false === strpos( $content, 'ai-faq-item' ) ) {
        return $graph; // geen FAQ-blok op deze pagina
    }

    $dom = new DOMDocument();
    libxml_use_internal_errors( true ); // negeer HTML5-warnings van DOMDocument
    $dom->loadHTML( '<?xml encoding="utf-8" ?>' . apply_filters( 'the_content', $content ) );
    libxml_clear_errors();

    $xpath     = new DOMXPath( $dom );
    $items     = $xpath->query( "//div[contains(@class,'ai-faq-item')]" );
    $questions = array();

    foreach ( $items as $item ) {
        $q = $xpath->query( './/h3', $item )->item( 0 );
        $a = $xpath->query( ".//p[contains(@class,'with-margin')]", $item )->item( 0 );

        if ( ! $q || ! $a ) {
            continue;
        }

        $questions[] = array(
            '@type'          => 'Question',
            'name'           => trim( $q->textContent ),
            'acceptedAnswer' => array(
                '@type' => 'Answer',
                'text'  => trim( $a->textContent ),
            ),
        );
    }

    if ( empty( $questions ) ) {
        return $graph;
    }

    $graph[] = array(
        '@type'      => 'FAQPage',
        'mainEntity' => $questions,
    );

    return $graph;
}

Dubbele schema voorkomen (plugin en handmatig samen)

Laat je Yoast of Rank Math actief draaien voor andere taken, en voeg je zelf ook JSON-LD toe, dan ontstaat al snel dubbele of conflicterende schema: twee Organization-blokken, of een Article vanuit de plugin naast jouw eigen versie[12]. Google negeert dat niet automatisch; het kan de parser in verwarring brengen en rich results juist blokkeren in plaats van opleveren.

Controleer dit altijd voordat je zelf iets toevoegt:

  1. Open een pagina en gebruik "bekijk paginabron" (view-source) in je browser.
  2. Zoek op application/ld+json.
  3. Vind je al een blok van je SEO-plugin, schakel dan de schema-module van die plugin uit (bij Yoast en Rank Math zit dit in de schema-instellingen per contenttype) voordat je jouw eigen code activeert.

Herhaal deze check na elke grote plugin-update: instellingen die je hebt uitgeschakeld, worden bij sommige updates stilletjes teruggezet.

Testen en valideren

Publiceer nooit schema markup zonder te testen. Twee gratis tools zijn hiervoor het startpunt:

  • Rich Results Test van Google zelf, toont precies welke rich result je pagina kwalificeert en welke velden ontbreken.
  • Schema Markup Validator, strenger op de Schema.org-specificatie zelf, los van wat Google er specifiek mee doet.

Krijg je een foutmelding over een ontbrekend verplicht veld, ga dan terug naar je PHP-array en vul het aan: vrijwel elke foutmelding wijst naar een specifiek veld dat je bent vergeten. Krijg je een waarschuwing over een mismatch tussen zichtbare content en markup, bijvoorbeeld een auteursnaam in de schema die niet op de pagina zelf staat, pas dan de zichtbare tekst aan of corrigeer de schema. Nooit allebei los van elkaar laten staan.

Performance-impact in de praktijk

De cijfers uit het begin van dit artikel vertalen zich naar een concreet voor/na-beeld. Vóór de wissel: een SEO-plugin die 12 tot 33KB extra paginagewicht en 92 tot 148ms Total Blocking Time toevoegt[9], met bij sommige plugins tot 15 extra databasequeries en 200 tot 400ms extra TTFB per paginaload[10]. Ná de wissel: een <script>-blok van minder dan 1KB, gegenereerd door PHP dat al bij elke paginaload draait, zonder extra databasequeries[2].

Dat verschil is het grootst op shared hostingKeurigOnline zegtShared hostingShared hosting is een hostingvorm waarbij meerdere websites dezelfde serverresources delen., waar elke extra databasequery en elke kilobyte PHPKeurigOnline zegtPHPPHP (Hypertext Preprocessor) is een populaire open-source scripttaal die speciaal is ontworpen voor webontwikkeling en server-side programmering.-verwerking direct in de TTFB terugkomt. Combineer je dit met server-side caching en een moderne PHP-versie, zoals we beschrijven in WordPress website sneller maken, dan tel je de winst van beide optimalisaties bij elkaar op.

Veelgemaakte fouten

  • Dubbele schema: plugin en handmatige code allebei actief voor hetzelfde type. Controleer altijd eerst via view-source.
  • Verplichte velden vergeten: een Article zonder datePublished of een Product zonder offers kwalificeert simpelweg niet voor een rich result[12].
  • Schema die niet overeenkomt met zichtbare content: een auteursnaam of prijs in de markup die nergens op de pagina zelf staat. Google beschouwt dit als misleidend.
  • Code buiten een child theme of mu-plugin plaatsen: verdwijnt bij de eerstvolgende thema-update, zonder waarschuwing[2].
  • Statische in plaats van dynamische velden: een hardcoded titel of datum in plaats van get_the_title() en get_the_date(), waardoor de schema bij het volgende artikel niet meer klopt.

Alles samen: het complete codebestand en waar je het plakt

Je hebt nu zes builders gezien: elk registreert zichzelf bij het ko_schema_graph-filter, bewaakt zijn eigen paginaconditie en levert alleen dynamische velden aan. Hieronder staan ze in één werkend bestand, in de volgorde waarin je ze hierboven tegenkwam, zodat je niet zelf hoeft te knippen en plakken uit losse codeblokken.

Kort overzicht van wat elke builder dekt en waar hij actief wordt:

  • Organization en WebSite: sitebreed, alleen op de homepage.
  • BreadcrumbList: elke pagina behalve de homepage, opgebouwd uit de echte categorie- of paginaboom.
  • Article/BlogPosting: elk artikel, gekoppeld aan de Organization-node via @id.
  • Product: alleen WooCommerce-productpagina's; de functie slaat zichzelf automatisch over als WooCommerce niet actief is.
  • FAQPage: elk artikel met een .ai-faq-item-blok, rechtstreeks uit de zichtbare content gelezen, dus nooit los van de tekst op de pagina.

Kopieer dit hele blok in één nieuw bestand. Laat de <?php-openingstag bovenaan staan als je een los bestand aanmaakt (optie A hieronder); laat hem juist weg als je de code onderaan een bestaande functions.php plakt (optie B).

<?php
/**
 * Schema.org JSON-LD voor deze site.
 * Bouwt één @graph met alle onderstaande types, gehaakt op wp_head.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Directe toegang tot dit bestand blokkeren.
}

/**
 * Verwijdert lege/null-velden uit een schema-node, zodat je nooit
 * een ongeldig "image": null of "sku": "" in de output krijgt.
 */
function ko_strip_empty( array $node ) {
    return array_filter(
        $node,
        static function ( $value ) {
            return null !== $value && '' !== $value && array() !== $value;
        }
    );
}

/**
 * De motor: verzamelt alle geregistreerde schema-nodes en print ze
 * als één @graph. Andere functies breiden dit uit via het
 * 'ko_schema_graph' filter, zonder deze functie te wijzigen.
 */
function ko_render_schema_graph() {
    $graph = apply_filters( 'ko_schema_graph', array() );
    $graph = array_values( array_filter( $graph ) );

    if ( empty( $graph ) ) {
        return;
    }

    $document = array(
        '@context' => 'https://schema.org',
        '@graph'   => $graph,
    );

    echo '<script type="application/ld+json">' .
        wp_json_encode( $document, JSON_UNESCAPED_SLASHES ) .
        '</script>' . "\n";
}
add_action( 'wp_head', 'ko_render_schema_graph' );

/** 1. Organization, sitebreed op de homepage. */
add_filter( 'ko_schema_graph', 'ko_schema_organization' );
function ko_schema_organization( $graph ) {
    if ( ! is_front_page() ) {
        return $graph;
    }

    $graph[] = ko_strip_empty( array(
        '@type' => 'Organization',
        '@id'   => home_url( '/#organization' ),
        'name'  => get_bloginfo( 'name' ),
        'url'   => home_url( '/' ),
        'logo'  => get_site_icon_url(),
    ) );

    return $graph;
}

/** 2. WebSite, met zoekactie op de homepage. */
add_filter( 'ko_schema_graph', 'ko_schema_website' );
function ko_schema_website( $graph ) {
    if ( ! is_front_page() ) {
        return $graph;
    }

    $graph[] = array(
        '@type'           => 'WebSite',
        '@id'             => home_url( '/#website' ),
        'name'            => get_bloginfo( 'name' ),
        'url'             => home_url( '/' ),
        'potentialAction' => array(
            '@type'       => 'SearchAction',
            'target'      => home_url( '/?s={search_term_string}' ),
            'query-input' => 'required name=search_term_string',
        ),
    );

    return $graph;
}

/** 3. BreadcrumbList, dynamisch per pagina opgebouwd. */
add_filter( 'ko_schema_graph', 'ko_schema_breadcrumbs' );
function ko_schema_breadcrumbs( $graph ) {
    if ( is_front_page() ) {
        return $graph;
    }

    $items   = array();
    $items[] = array( __( 'Home', 'ko' ), home_url( '/' ) );

    if ( is_single() ) {
        $cats = get_the_category();
        if ( ! empty( $cats ) ) {
            $items[] = array( $cats[0]->name, get_category_link( $cats[0]->term_id ) );
        }
        $items[] = array( get_the_title(), get_permalink() );
    } elseif ( is_page() ) {
        foreach ( array_reverse( get_post_ancestors( get_the_ID() ) ) as $ancestor_id ) {
            $items[] = array( get_the_title( $ancestor_id ), get_permalink( $ancestor_id ) );
        }
        $items[] = array( get_the_title(), get_permalink() );
    } else {
        return $graph; // archieven e.d. hebben geen vast pad, sla over
    }

    $list_items = array();
    foreach ( $items as $position => $item ) {
        $list_items[] = array(
            '@type'    => 'ListItem',
            'position' => $position + 1,
            'name'     => $item[0],
            'item'     => $item[1],
        );
    }

    $graph[] = array(
        '@type'           => 'BreadcrumbList',
        'itemListElement' => $list_items,
    );

    return $graph;
}

/** 4. Article/BlogPosting, gekoppeld aan de Organization-node. */
add_filter( 'ko_schema_graph', 'ko_schema_article' );
function ko_schema_article( $graph ) {
    if ( ! is_single() ) {
        return $graph;
    }

    $image_id = get_post_thumbnail_id();

    $graph[] = ko_strip_empty( array(
        '@type'            => 'BlogPosting',
        'headline'         => get_the_title(),
        'image'            => $image_id ? wp_get_attachment_image_url( $image_id, 'full' ) : null,
        'author'           => array(
            '@type' => 'Person',
            'name'  => get_the_author(),
        ),
        'publisher'        => array( '@id' => home_url( '/#organization' ) ),
        'datePublished'    => get_the_date( 'c' ),
        'dateModified'     => get_the_modified_date( 'c' ),
        'mainEntityOfPage' => get_permalink(),
    ) );

    return $graph;
}

/** 5. Product, met live prijs en voorraad uit WooCommerce. */
add_filter( 'ko_schema_graph', 'ko_schema_product' );
function ko_schema_product( $graph ) {
    if ( ! function_exists( 'is_product' ) || ! is_product() ) {
        return $graph; // WooCommerce niet actief, of geen productpagina
    }

    global $product;
    if ( ! $product instanceof WC_Product ) {
        return $graph;
    }

    $node = ko_strip_empty( array(
        '@type'  => 'Product',
        'name'   => $product->get_name(),
        'image'  => wp_get_attachment_image_url( $product->get_image_id(), 'full' ),
        'sku'    => $product->get_sku(),
        'offers' => array(
            '@type'         => 'Offer',
            'priceCurrency' => get_woocommerce_currency(),
            'price'         => $product->get_price(),
            'availability'  => $product->is_in_stock()
                ? 'https://schema.org/InStock'
                : 'https://schema.org/OutOfStock',
            'url'           => get_permalink( $product->get_id() ),
        ),
    ) );

    if ( $product->get_review_count() > 0 ) {
        $node['aggregateRating'] = array(
            '@type'       => 'AggregateRating',
            'ratingValue' => $product->get_average_rating(),
            'reviewCount' => $product->get_review_count(),
        );
    }

    $graph[] = $node;
    return $graph;
}

/** 6. FAQPage, automatisch uit .ai-faq-item-blokken in de content. */
add_filter( 'ko_schema_graph', 'ko_schema_faq_from_content' );
function ko_schema_faq_from_content( $graph ) {
    if ( ! is_single() ) {
        return $graph;
    }

    $content = get_the_content();
    if ( false === strpos( $content, 'ai-faq-item' ) ) {
        return $graph; // geen FAQ-blok op deze pagina
    }

    $dom = new DOMDocument();
    libxml_use_internal_errors( true ); // negeer HTML5-warnings van DOMDocument
    $dom->loadHTML( '<?xml encoding="utf-8" ?>' . apply_filters( 'the_content', $content ) );
    libxml_clear_errors();

    $xpath     = new DOMXPath( $dom );
    $items     = $xpath->query( "//div[contains(@class,'ai-faq-item')]" );
    $questions = array();

    foreach ( $items as $item ) {
        $q = $xpath->query( './/h3', $item )->item( 0 );
        $a = $xpath->query( ".//p[contains(@class,'with-margin')]", $item )->item( 0 );

        if ( ! $q || ! $a ) {
            continue;
        }

        $questions[] = array(
            '@type'          => 'Question',
            'name'           => trim( $q->textContent ),
            'acceptedAnswer' => array(
                '@type' => 'Answer',
                'text'  => trim( $a->textContent ),
            ),
        );
    }

    if ( empty( $questions ) ) {
        return $graph;
    }

    $graph[] = array(
        '@type'      => 'FAQPage',
        'mainEntity' => $questions,
    );

    return $graph;
}

Waar je dit precies neerzet (de achtergrond staat in Voorbereiding hierboven):

  • Optie A, mu-plugin (aanbevolen): plak het blok inclusief de <?php-regel in een nieuw bestand wp-content/mu-plugins/ko-schema-markup.php. Bestaat de map mu-plugins nog niet, maak hem dan aan. WordPressKeurigOnline zegtWordPressWordPress is een open-source contentmanagementsysteem (CMS) geschreven in PHP, waarmee je eenvoudig websites en blogs kunt bouwen en beheren. laadt alles hierin automatisch, thema-onafhankelijk en niet uit te schakelen.
  • Optie B, child theme: plak alleen de functies (dus zonder de <?php-regel bovenaan, die staat al in je bestaande bestand) onderaan de functions.php van je child theme.

Na het plaatsen: bezoek de homepage en een los artikel, bekijk de paginabron en zoek op application/ld+json om te controleren of het @graph-blok verschijnt. Heb je WooCommerce, controleer dan ook een productpagina. Loop daarna de stappen uit Testen en valideren hierboven na voordat je de bijbehorende schema-module van je SEO-plugin uitschakelt.

Veelgestelde vragen

Vertraagt schema markup mijn WordPress-site?

Handmatige JSON-LD via functions.php weegt doorgaans minder dan 1KB en heeft vrijwel geen meetbare invloed op de laadtijd[2]. Een zware SEO-plugin die schema dynamisch en ongecachet opbouwt, kan wel 40 tot 80KB en honderden milliseconden aan TTFBKeurigOnline zegtTTFBTTFB (Time to First Byte) meet de tijd tussen een HTTP-verzoek en de eerste byte van het serverantwoord. toevoegen[10].

Wat is het verschil tussen JSON-LD, Microdata en RDFa, en waarom kiest Google voor JSON-LD?

JSON-LD staat los van je zichtbare HTML in een eigen <script>-blok; Microdata en RDFa moet je middenin je bestaande HTMLKeurigOnline zegtHTMLHTML is de programmeertaal voor het maken van webpagina’s.-attributen verweven[6]. Google noemt JSONKeurigOnline zegtJSONJSON is een lichtgewicht dataformaat voor het uitwisselen van gegevens tussen systemen.-LD expliciet als aanbevolen formaat omdat het voor de meeste website-eigenaren het makkelijkst te implementeren en onderhouden is[1].

Kan ik een SEO-plugin zoals Yoast of Rank Math volledig vervangen door handmatige schema-code?

Voor schema markup alleen: ja, mits je sitestructuur voorspelbaar is (blog, bedrijfssite). Voor de rest van wat een SEO-plugin doet, zoals meta titles, sitemaps en redirects, is een plugin meestal nog steeds de praktischste keuze, zeker bij WooCommerceKeurigOnline zegtWooCommerceWooCommerce is de gratis e-commerce plugin voor WordPress. Bouw een professionele webshop met producten, betalingen en verzending — zonder technische kennis.-producten of niet-technische beheerders.

Waar plaats ik JSON-LD-code: functions.php, header.php of een mu-plugin?

Nooit in het functions.php of header.php van je hoofdthema: die worden overschreven bij een thema-update. Gebruik een child theme voor site-specifieke code, of een mu-plugin in wp-content/mu-plugins/ als je thema-onafhankelijkheid wilt[2].

Wat gebeurt er als ik zowel een plugin als handmatige schema tegelijk gebruik?

Dan ontstaat dubbele of conflicterende schema voor hetzelfde contenttype[12]. Controleer altijd eerst via "bekijk paginabron" of application/ld+json al aanwezig is, en schakel de schema-module van je plugin uit voordat je zelf iets toevoegt.

Welke schema-types heeft een gemiddelde WordPress-site echt nodig?

Organization en WebSite sitebreed, Article of BlogPosting op elk artikel, en BreadcrumbList op pagina's met een navigatiepad[11]. FAQ-rich results worden sinds 2023 door Google grotendeels beperkt tot overheids- en gezondheidssites, maar de markup blijft waardevol voor AI-antwoordsystemen[13].

Bronnen