Skip to content

marcelbest/json-ld-php-for-wordpress

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Connected JSON-LD for WordPress (PHP, no plugin)

A single, fully cross-linked Schema.org @graph per page for classic WordPress themes — one PHP file, no plugin, no build step.

Instead of scattering isolated JSON-LD snippets, this emits one <script type="application/ld+json"> block whose nodes reference each other by stable @ids. It also derives a FAQPage node automatically from a Core Accordion block and marks the headline and lead as Speakable for answer engines.

Full walkthrough (German): https://marcelbest.com/webdesign/2026/json-ld-fuer-wordpress-ohne-plugin/ The blog post explains the how and why in detail; this README is the quick start.

What it outputs

One @graph, with nodes chosen by context:

Context Nodes
Always WebSite, Person
Single post BlogPosting (with SpeakableSpecification; plus a linked FAQPage when an Accordion is present)
Front page Blog
Category / tag / taxonomy archive CollectionPage
Static page WebPage
Any page with a multi-level path BreadcrumbList

All nodes are wired together: BlogPosting.isPartOfWebSite, author/publisherPerson, FAQPage.aboutBlogPosting, and so on. No duplicated, orphaned markup.

Highlights

  • Automatic FAQPage from the Accordion block. The Core core/accordion block is treated as your FAQ section. Its presence alone produces a FAQPage node — no extra authoring markup. Parsing is fully server-side (parse_blocks()render_block()DOMXPath), so any client-side rewrite of the accordion is irrelevant.
  • Speakable. Headline and lead are marked as suitable for text-to-speech via CSS selectors, aimed at modern answer engines (Google AI Overviews, Perplexity, ChatGPT Search) and featured-snippet eligibility.
  • Data-driven, no empty fields. Optional fields (image, keywords, description, social sameAs) are only emitted when real data exists.
  • No dependencies. No SEO plugin required; everything site-specific is overridable via filters.

Requirements

  • A classic theme (PHP functions.php). Block themes / full site editing are not supported without changes.
  • PHP 8.0+
  • The automatic FAQPage requires the Core Accordion block (WordPress 6.9+).

Installation

  1. Copy json-ld.php into your theme, e.g. inc/json-ld.php.
  2. Require it from your theme's functions.php:
    require get_template_directory() . '/inc/json-ld.php';
  3. Replace the yourtheme_ function prefix, the YOURTHEME_ constant prefix and the yourtheme text domain with your own namespace.
  4. Configure your identity via the filters below (at minimum the Person name and social links).

No permalink flush or activation step is needed — output happens on wp_head.

Configuration (filters)

Everything site-specific is a filter. Drop these into functions.php.

// Person / author identity (defaults are derived from the site).
add_filter( 'yourtheme_jsonld_person_name', fn() => 'Jane Doe' );
add_filter( 'yourtheme_jsonld_person_url',  fn() => home_url( '/about/' ) );
add_filter( 'yourtheme_jsonld_person_id',   fn() => home_url( '/about/#jane-doe' ) );

// Social profiles → Person.sameAs (array of URLs).
add_filter( 'yourtheme_jsonld_person_social', fn() => array(
    'https://github.com/janedoe',
    'https://www.linkedin.com/in/janedoe',
) );
Filter / constant Default Purpose
yourtheme_jsonld_person_name site name Person name
yourtheme_jsonld_person_url home URL Person url
yourtheme_jsonld_person_id {home}/#person Person @id (must be stable)
yourtheme_jsonld_person_social [] Array of profile URLs → sameAs
yourtheme_jsonld_description excerpt / term description Plug in your SEO plugin's description
yourtheme_jsonld_speakable_selectors ['.post-header h1', '.post-excerpt'] CSS selectors for Speakable — match your theme's markup
yourtheme_jsonld_faq_block accordion Block base name parsed for FAQ
yourtheme_jsonld_blog_url posts page or home URL/@id base for the Blog node
yourtheme_jsonld_breadcrumb_items derived Adjust breadcrumb trail
yourtheme_jsonld_remove_hentry true Strip the legacy hentry microformat class
yourtheme_jsonld_graph full graph Final escape hatch — modify any node before output
YOURTHEME_JSONLD_SEPARATOR (constant) | Separator in page/collection names

Set the Person identity — don't ship the default

The Person node is the author and publisher of everything, so its name matters. To avoid an empty field it defaults to get_bloginfo('name') (the site name) — but a site name is not necessarily a person's name. On a site called "Acme Blog" the default produces a Person named "Acme Blog", which is semantically wrong. Treat yourtheme_jsonld_person_name (and the rest of the Person identity) as a required setting whenever your site name isn't your own name.

If the publishing entity is a company rather than a person, an Organization node fits better than Person. Swap it via the yourtheme_jsonld_graph filter:

add_filter( 'yourtheme_jsonld_graph', function( $graph ) {
    foreach ( $graph as &$node ) {
        if ( isset( $node['@type'] ) && 'Person' === $node['@type'] ) {
            $node['@type'] = 'Organization';
            $node['logo']  = home_url( '/logo.png' );
        }
    }
    return $graph;
} );

Speakable needs matching markup

Speakable points at on-page elements, not meta fields. The defaults (.post-header h1, .post-excerpt) assume the title sits in .post-header and the lead is rendered as a visible .post-excerpt element. Adjust yourtheme_jsonld_speakable_selectors to your theme, and make sure the lead is actually rendered on the page.

Using an SEO plugin's description

add_filter( 'yourtheme_jsonld_description', function( $description ) {
    // e.g. Yoast
    if ( function_exists( 'YoastSEO' ) ) {
        $meta = YoastSEO()->meta->for_current_page()->description;
        if ( $meta ) {
            return $meta;
        }
    }
    return $description;
} );

The FAQPage convention

The Accordion block is treated as your FAQ section by convention — write your FAQ as a Core Accordion and a FAQPage node appears automatically. Each item's heading becomes a Question, its panel the acceptedAnswer (plain text; links are reduced to their text). No Accordion, no FAQ node, no error.

If you use the Accordion block for non-FAQ content too, point yourtheme_jsonld_faq_block at a different block, or return [] from a custom integration.

Note: Valid markup means eligible, not guaranteed. Google shows FAQ rich results in the SERP only for limited accounts — but for LLM/answer-engine parsing the structured FAQ stays valuable regardless.

Validation

Check your output with:

License

GPL-2.0-or-later. See LICENSE.

About

One cross-linked Schema.org JSON-LD @graph per page for classic WordPress themes — auto FAQPage from the Accordion block, Speakable, no plugin.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages