Contents
Introduction
This article is a comprehensive, detailed tutorial on how to insert structured data JSON-LD into a WordPress site from PHP. It covers concepts, best practices, WordPress hooks, dynamic data retrieval, escaping and encoding, common schema types (Organization, WebSite, Article, BreadcrumbList, Product, FAQPage, HowTo), advanced patterns (multiple graphs, conditional loading, caching), WooCommerce specifics, multilingual considerations, testing and debugging, common pitfalls, and ready-to-use code examples you can paste into a theme or plugin. Code examples are provided in full and use PHP and JSON as appropriate. All JSON-LD output examples are shown inside the HTML n }
Best practices when generating JSON-LD in PHP
- Always use wp_json_encode() or json_encode() with safe flags — this ensures proper escaping and avoids broken JSON due to quotes or special characters.
- Provide ISO 8601 dates — use get_the_date(c) or mysql2date(c, get_post_time( Y-m-d H:i:s, true, post ) ).
- Include image URLs with absolute paths — use wp_get_attachment_image_src( get_post_thumbnail_id( post ), full )[0] or wp_get_attachment_url().
- Select the right @type — Article, BlogPosting, NewsArticle for content Product for products Organization, LocalBusiness for businesses BreadcrumbList for breadcrumbs FAQPage or QAPage for FAQs.
- Don’t duplicate contradictory data — if you output a WebSite schema and a WebPage/Article schema, make sure fields like name and url are consistent.
- Use @graph to combine multiple entities when you want to output several top-level objects in one script block.
- Keep JSON-LD readable — use JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE flags so URLs and Unicode characters remain readable.
Common schema types and properties
Below are examples for the most common schema types with recommended properties to include. Use these as a checklist when constructing your JSON-LD.
1) Organization/WebSite
Site-level data: organization name, logo, contact, and optional SearchAction for site search integration with Google.
add_action( wp_head, mytheme_website_schema ) function mytheme_website_schema() { schema = array( @context => https://schema.org, @graph => array( array( @type => Organization, @id => get_home_url() . #organization, name => get_bloginfo( name ), url => get_home_url(), logo => array( @type => ImageObject, url => get_theme_mod( custom_logo ) ? wp_get_attachment_image_src( get_theme_mod( custom_logo ), full )[0] : ) ), array( @type => WebSite, @id => get_home_url() . #website, url => get_home_url(), name => get_bloginfo( name ), potentialAction => array( @type => SearchAction, target => get_home_url() . /?s={search_term_string}, query-input => required name=search_term_string ) ) ) ) echo n }
2) Article / BlogPosting
For posts and pages that are articles. Include headline, description, image, datePublished, dateModified, author, publisher. Use get_the_post_thumbnail_url() for images and get_the_excerpt() for description (or manually craft a meta description).
add_action( wp_head, mytheme_article_schema ) function mytheme_article_schema() { if ( ! is_single() ) { return } global post author_name = get_the_author_meta( display_name, post->post_author ) image = get_the_post_thumbnail_url( post, full ) article = array( @context => https://schema.org, @type => Article, mainEntityOfPage => array( @type => WebPage, @id => get_permalink( post ) ), headline => get_the_title( post ), image => image ? array( image ) : array(), datePublished => get_the_date( c, post ), dateModified => get_the_modified_date( c, post ), author => array( @type => Person, name => author_name ), publisher => array( @type => Organization, name => get_bloginfo( name ), logo => array( @type => ImageObject, url => get_theme_mod( custom_logo ) ? wp_get_attachment_image_src( get_theme_mod( custom_logo ), full )[0] : ) ), description => get_the_excerpt( post ) ) echo n }
3) BreadcrumbList
Breadcrumbs help understand page hierarchy. Generate using a list of positions with item id and name. Use get_post_ancestors for hierarchical content.
add_action( wp_head, mytheme_breadcrumb_schema ) function mytheme_breadcrumb_schema() { if ( ! ( is_singular() is_category() is_tag() ) ) { return } global post breadcrumbs = array() position = 1 breadcrumbs[] = array( @type => ListItem, position => position , name => Home, item => get_home_url() ) if ( is_singular() post->post_type === post ) { terms = get_the_terms( post, category ) if ( terms ! is_wp_error( terms ) ) { cat = current( terms ) ancestors = get_ancestors( cat->term_id, category ) ancestors = array_reverse( ancestors ) foreach ( ancestors as ancestor_id ) { ancestor = get_category( ancestor_id ) breadcrumbs[] = array( @type => ListItem, position => position , name => ancestor->name, item => get_category_link( ancestor_id ) ) } breadcrumbs[] = array( @type => ListItem, position => position , name => cat->name, item => get_category_link( cat->term_id ) ) } breadcrumbs[] = array( @type => ListItem, position => position , name => get_the_title( post ), item => get_permalink( post ) ) } schema = array( @context => https://schema.org, @type => BreadcrumbList, itemListElement => breadcrumbs ) echo n }
4) Product (WooCommerce)
For WooCommerce, use the product object to pull price, availability, currency, SKU, brand, images. Use wc_get_product() rather than direct post meta where possible.
add_action( wp_head, mytheme_woocommerce_product_schema ) function mytheme_woocommerce_product_schema() { if ( ! function_exists( is_product ) ! is_product() ) { return } global product if ( ! product ) { product = wc_get_product( get_the_ID() ) } if ( ! product ) { return } image = wp_get_attachment_image_src( product->get_image_id(), full ) schema = array( @context => https://schema.org, @type => Product, name => product->get_name(), image => image ? array( image[0] ) : array(), description => wp_strip_all_tags( product->get_description() ), sku => product->get_sku(), offers => array( @type => Offer, url => get_permalink( product->get_id() ), priceCurrency => get_woocommerce_currency(), price => product->get_price(), availability => product->is_in_stock() ? https://schema.org/InStock : https://schema.org/OutOfStock, itemCondition => https://schema.org/NewCondition ) ) echo n }
5) FAQPage
If you have FAQ content on a page and want it eligible for FAQ rich results, output a FAQPage with a mainEntity array of Question/Answer pairs. Make sure questions are visible on the page.
add_action( wp_head, mytheme_faq_schema ) function mytheme_faq_schema() { if ( ! is_page() ) { return } // Example: FAQ stored in post meta as an array of arrays [ [ q => , a => ], ... ] faqs = get_post_meta( get_the_ID(), _my_faqs, true ) if ( empty( faqs ) ! is_array( faqs ) ) { return } questions = array() foreach ( faqs as faq ) { if ( empty( faq[q] ) empty( faq[a] ) ) { continue } questions[] = array( @type => Question, name => wp_strip_all_tags( faq[q] ), acceptedAnswer => array( @type => Answer, text => wp_strip_all_tags( faq[a] ), ) ) } if ( empty( questions ) ) { return } schema = array( @context => https://schema.org, @type => FAQPage, mainEntity => questions ) echo n }
Advanced patterns
Using @graph to combine multiple objects
When you want to output Organization, WebSite, WebPage/Article, BreadcrumbList, etc., you can place them inside a single JSON-LD document with @graph. This reduces the number of script tags and clearly connects entities using @id references.
add_action( wp_head, mytheme_graph_schema ) function mytheme_graph_schema() { if ( ! is_singular() ) { return } global post site_url = get_home_url() org_id = site_url . #organization web_id = get_permalink( post ) . #webpage graph = array( array( @type => Organization, @id => org_id, name => get_bloginfo( name ), url => site_url, ), array( @type => WebPage, @id => web_id, url => get_permalink( post ), name => get_the_title( post ), isPartOf => array( @id => org_id ) ) ) schema = array( @context => https://schema.org, @graph => graph ) echo n }
Conditional loading and avoiding duplicate schema
- Avoid plugins and themes both outputting similar schema. If you add schema in a theme, check if a plugin has already output similar data. For example, test for a constant or a function from common plugins, or provide a filter so plugin authors or site admins can disable the theme output.
- Allow filtering of your schema array before encoding so other code can modify it: apply_filters(mytheme_schema_article, schema).
- Use conditional tags like is_singular(), is_front_page(), is_home(), is_product(), is_archive() to control where schema appears.
Caching expensive schema generation
For schemas that require expensive meta or external API calls (e.g., pulling ratings from a 3rd-party), cache the generated JSON in a transient and refresh it periodically.
function mytheme_cached_schema_for_product( product_id ) { transient_key = schema_product_ . product_id json = get_transient( transient_key ) if ( json ) { return json } product = wc_get_product( product_id ) if ( ! product ) { return } schema = array( @context => https://schema.org, @type => Product, name => product->get_name(), offers => array( @type => Offer, price => product->get_price(), priceCurrency => get_woocommerce_currency(), ) ) json = wp_json_encode( schema, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE ) // Cache for 12 hours set_transient( transient_key, json, 12 HOUR_IN_SECONDS ) return json } add_action( wp_head, function() { if ( function_exists( is_product ) is_product() ) { echo n } })
Escaping and encoding details
- wp_json_encode() is preferred because it mirrors WordPresss internal JSON encoding behavior and is compatible with WPs JSON handling.
- Use flags JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE to keep URLs and Unicode characters readable. Optionally use JSON_PRETTY_PRINT for easier debugging (but it increases output size).
- Avoid echoing PHP strings that contain user input without cleaning. Use wp_strip_all_tags() for descriptions or other HTML-laden strings if you want plain text. For names and titles, use single escaping functions suitable for JSON (wp_json_encode will escape as JSON requires).
- Do not escape JSON with esc_html() or esc_attr() before encoding — encode the PHP array and then output. If you need to ensure HTML safety (rare for JSON-LD), wrap the full JSON string in esc_html() when embedding in an HTML attribute, but regular script tag content should remain raw JSON-LD.
Schema for multilingual sites
- Specify language where appropriate using the inLanguage property for Article and WebPage types: inLanguage => get_locale() or get_bloginfo(language). For multi-lingual sites use the page’s current language.
- If you have alternate language versions of pages, include alternateName or use the WebPage property alternateName or provide separate WebPage entries for each language with the corresponding URL.
- For hreflang, you should continue to use link rel=alternate hreflang=x as usual Schema.org doesnt replace hreflang but you can express language inside the JSON-LD object.
Testing and debugging structured data
- View source and verify script tags are present and JSON is valid JSON. Use browser console or online JSON validators.
- Use Googles Rich Results Test: https://search.google.com/test/rich-results. It can tell you what rich result types are detected and list errors/warnings.
- Use the Schema Markup Validator: https://validator.schema.org/ (based on schema.org validator).
- In Google Search Console, check the Enhancements reports for structured data errors after search engine crawls your site.
- Test on staging before deploying into production. Remember Search Console data may take time to update.
Common pitfalls and how to avoid them
- Mismatch between page content and structured data: Schema should reflect what a user sees. Dont include attributes (like price, availability, ratings) if they are not visible or accurate on the page.
- Duplicate or conflicting schema: Multiple plugins/themes adding similar schema may produce conflicting outputs. Provide filters or use theme settings to disable duplicates.
- Broken JSON due to manual concatenation: Never build JSON strings by manual concatenation of quoted pieces. Always build arrays/objects and encode with wp_json_encode.
- Using deprecated types or incorrect fields: Always refer to Schema.org and Google docs for required and recommended fields: https://schema.org/ and https://developers.google.com/search/docs/appearance/structured-data.
- Excessive or irrelevant structured data: Only include schema types that are relevant to the page content (e.g., dont mark a page as a Product if its not a product page).
Checklist before deploying JSON-LD from PHP
- Schema type matches the page content (Article, Product, LocalBusiness, etc.).
- Required properties for each type are included and accurate (headline, datePublished for Article price, priceCurrency for offers).
- Images are absolute URLs and accessible to crawlers.
- Dates are in ISO 8601 format (use c date format in PHP).
- Schema does not contradict visible content.
- JSON is valid and passes Rich Results Test and Schema Validator.
- Performance optimized: expensive operations cached and code not running when not needed.
- Provision a filter or action to allow disabling or modifying the schema output.
Complete real-world example: Article Breadcrumb Organization in one output
This example demonstrates building a combined JSON-LD using @graph to represent organization (site), webpage/article, and breadcrumb list. It can be dropped into a themes functions.php or into a plugin. It uses wp_json_encode and includes sanity checks.
add_action( wp_head, mytheme_combined_schema ) function mytheme_combined_schema() { if ( ! is_singular() ) { return } global post if ( ! post ) { return } site_url = get_home_url() org_id = site_url . #organization webpage_id = get_permalink( post ) . #webpage // Organization org = array( @type => Organization, @id => org_id, name => get_bloginfo( name ), url => site_url, logo => array( @type => ImageObject, url => get_theme_mod( custom_logo ) ? wp_get_attachment_image_src( get_theme_mod( custom_logo ), full )[0] : ) ) // Article / WebPage image = get_the_post_thumbnail_url( post, full ) article = array( @type => Article, @id => webpage_id, mainEntityOfPage => array( @type => WebPage, @id => get_permalink( post ) ), headline => get_the_title( post ), image => image ? array( image ) : array(), datePublished => get_the_date( c, post ), dateModified => get_the_modified_date( c, post ), author => array( @type => Person, name => get_the_author_meta( display_name, post->post_author ) ), publisher => array( @id => org_id ), description => wp_strip_all_tags( get_the_excerpt( post ) ) ) // Breadcrumb breadcrumbs = array() position = 1 breadcrumbs[] = array( @type => ListItem, position => position , name => Home, item => site_url ) // For posts, add category breadcrumbs if ( post->post_type === post ) { cats = get_the_category( post->ID ) if ( cats ) { cat = cats[0] anc = get_ancestors( cat->term_id, category ) anc = array_reverse( anc ) foreach ( anc as a ) { c = get_category( a ) breadcrumbs[] = array( @type => ListItem, position => position , name => c->name, item => get_category_link( c->term_id ) ) } breadcrumbs[] = array( @type => ListItem, position => position , name => cat->name, item => get_category_link( cat->term_id ) ) } } breadcrumbs[] = array( @type => ListItem, position => position , name => get_the_title( post ), item => get_permalink( post ) ) breadcrumbList = array( @type => BreadcrumbList, itemListElement => breadcrumbs ) graph = array( org, article, breadcrumbList ) schema = array( @context => https://schema.org, @graph => graph ) // Allow third-party code to adjust the schema schema = apply_filters( mytheme_combined_schema, schema, post ) echo n }
Security and privacy considerations
- Do not include personal data in schema unless it is intentionally public and necessary for the page (e.g., public author name). Avoid embedding sensitive PII.
- Do not expose internal-only data, API keys, or private IDs in JSON-LD.
Maintenance and extensibility
- Expose filters before encoding arrays so other plugins or child themes can alter schema arrays (as done in the example with apply_filters(mytheme_combined_schema, schema, post)).
- Version your schema output if you expect major changes so you can debug what produced which markup (e.g., include a @context comment in logs or a version in plugin settings, not inside JSON-LD visible to search engines).
- Document where the schema is generated in your theme or plugin so future maintainers know where to look.
Quick reference: useful WordPress functions for schema
Function | Use |
wp_json_encode() | Encode PHP array to JSON safely |
get_the_title(), get_the_excerpt(), get_the_content() | Pull title, description, and content for Article/BlogPosting |
get_the_date(c), get_the_modified_date(c) | Return ISO 8601 formatted dates |
get_permalink() | Get current page URL |
wp_get_attachment_image_src(), get_the_post_thumbnail_url() | Get absolute image URL for schema image property |
wc_get_product() | Get product object in WooCommerce to extract price, SKU, etc. |
get_transient(), set_transient() | Cache generated schema JSON for performance |
Final notes
This tutorial has shown how to generate clean, accurate JSON-LD structured data in WordPress using PHP. The key rules are: build arrays in PHP, use wp_json_encode() with safe flags, output inside a script tag in the head (or footer when appropriate), ensure schema reflects visible page content, and test with Googles Rich Results Test and the Schema Markup Validator. Include filters for extensibility, cache expensive operations, and avoid duplicate or conflicting outputs.
Further reading
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |