wprichsnippets
Guide · WordPress · How-tosCERT No. WP 0006

Add schema to WordPress without a plugin

Schema type
Article Person Organization
Checked against
developer.wordpress.org reference and handbook pages and the PHP manual, read November 2026
Author
The wprichsnippets.com editors
Published
Updated
Direct answerField 01

To add schema markup to WordPress without a plugin, put a small PHP file in wp-content/mu-plugins, hook a function to wp_head, build the JSON-LD as a PHP array from post data, encode it with wp_json_encode using JSON_HEX_TAG, and print it in a script tag. Then test the live URL.

55 wordsUpdated November 8, 2026
FeatureFig. 01
A hand-cut blank card beside a ruler and craft knife on a cutting mat
Four parts: a hook, a guard, an array built from post data, and an encoder.

01When is hand-written JSON-LD better than a plugin?

Knowing how to add schema markup to WordPress by hand is useful even if you keep a plugin, because it shows what the plugin should be printing. Hand-written JSON-LD is markup you generate with your own PHP from your own data, instead of through a plugin's settings screen. It is the better choice in a few specific cases:

  • You need one type on one template, such as Course or SoftwareApplication, and nothing else.
  • A plugin prints markup you cannot configure or switch off for a type.
  • You want markup that changes only when you change it, with no surprises after updates.
  • You already store the data in custom fields and want the markup to read them directly.

None of these cases needs much code. The example in this guide is about forty lines, most of it the array itself.

It is the wrong choice when nobody on the site can maintain PHP. Code you write is code you own, including after the next WordPress release.

02Should the code go in an mu-plugin or functions.php?

A must-use plugin, or mu-plugin, is a PHP file in wp-content/mu-plugins that WordPress loads automatically. The WordPress must-use plugins documentation notes that mu-plugins cannot be disabled from wp-admin, load before normal plugins, and must sit directly in that folder, because files in subdirectories are not loaded automatically.

Must-use plugins are useful for code that should always run and should not be disabled accidentally from wp-admin.

WordPress Advanced Administration Handbook, Must-Use Plugins · developer.wordpress.org · read on
Where to put schema code in WordPress
OptionSurvives a theme changeCan be switched off in wp-adminBest for
Active theme functions.phpNoOnly by changing themeMarkup tied to that theme's own templates
Child theme functions.phpSurvives parent updates, not a theme changeOnly by changing themeSites committed to one theme
mu-pluginYesNoSite-wide markup that should always print
Regular pluginYesYesMarkup you want to toggle or reuse across sites

Schema describes content, not presentation, so it rarely belongs to the theme. An mu-plugin is the usual answer. The same documentation warns that mu-plugins get no update notifications and that a broken one can affect the whole site, so keep the file small, reviewed and documented.

03How to add schema markup to WordPress with an mu-plugin

The pattern has four parts: a hook, a guard, an array and an encoder. The wp_head hook is triggered inside the head section of the theme's header and prints scripts or data there, which makes it the conventional place for JSON-LD.

Example 1 · PHP: an mu-plugin that prints Article JSON-LD on single postsPHPDisplay only
1<?php2/**3 * Plugin Name: Example Article JSON-LD4 * Description: Prints Article structured data on single posts.5 */67add_action( 'wp_head', 'example_print_article_jsonld', 20 );189function example_print_article_jsonld() {10  if ( ! is_singular( 'post' ) ) {211    return;12  }1314  $post_id   = get_queried_object_id();15  $author_id = (int) get_post_field( 'post_author', $post_id );16  $title     = html_entity_decode( wp_strip_all_tags( get_the_title( $post_id ) ), ENT_QUOTES, 'UTF-8' );31718  $data = array(19    '@context'      => 'https://schema.org',20    '@type'         => 'Article',21    '@id'           => get_permalink( $post_id ) . '#article',22    'headline'      => $title,23    'datePublished' => get_the_date( 'c', $post_id ),24    'dateModified'  => get_the_modified_date( 'c', $post_id ),25    'author'        => array(26      '@type' => 'Person',27      'name'  => get_the_author_meta( 'display_name', $author_id ),28    ),29    'publisher'     => array(30      '@type' => 'Organization',31      'name'  => get_bloginfo( 'name' ),32      'url'   => home_url( '/' ),33    ),34  );3536  $json = wp_json_encode( $data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );437  if ( false === $json ) {38    return;39  }4041  echo '<script type="application/ld+json">' . $json . '</script>' . "\n";42}
  1. 1Line 7The priority only sets order inside the head; any value works.
  2. 2Line 10The guard keeps the markup off archives, pages and the home page.
  3. 3Line 16Titles pass through WordPress filters that can add HTML entities; decode them so the headline reads as plain text.
  4. 4Line 36Encodes every angle bracket, so the data can never close the script tag early.

Everything in the array comes from WordPress itself: the title, both dates, the author's display name and the site name. The markup therefore always matches what the post shows, which is the property that matters most. To extend the pattern to pages or a custom post type, change the guard, for example to is_singular( array( 'post', 'page' ) ), and adjust the type: a page about your organization might be an AboutPage rather than an Article. The block it prints looks like this, with example values:

Example 2 · the JSON-LD the mu-plugin prints, with example valuesJSON-LDDisplay only
1{2  "@context": "https://schema.org",3  "@type": "Article",4  "@id": "https://example.com/example-post/#article",5  "headline": "Example Post Title",6  "datePublished": "2026-10-01T09:00:00+00:00",7  "dateModified": "2026-11-08T09:00:00+00:00",18  "author": {9    "@type": "Person",10    "name": "Example Author"11  },12  "publisher": {13    "@type": "Organization",14    "name": "Example Site",15    "url": "https://example.com/"16  }17}
  1. 1Line 7Comes from the post's last modified date, so it moves only when the post is edited.

04How do you pull per-post data from custom fields?

Custom fields, or post meta, are key and value pairs WordPress stores against a post. They are the natural home for data that has no core field, such as a rating, a product name or a course duration. Read a single value with get_post_meta( $post_id, 'example_rating', true ); the final true asks for one value instead of an array. For a review kept in custom fields, the array gains a reviewRating with ratingValue and bestRating, and an itemReviewed object naming the thing reviewed.

  1. Store each value in its own field, for example example_rating and example_item_name.
  2. Read them inside the same function that builds the array.
  3. Skip the property, or the whole block, when a required value is empty. Missing data should mean missing markup, never a placeholder.
  4. Cast numbers with (float) so ratings are printed as numbers rather than strings.
  5. Show the same values in the visible post; markup must describe what readers can see.

If the markup grows into a review box with its own visible panel, the pattern changes slightly, because the box and the JSON-LD must then be rendered from the same values. The guide to review boxes, shortcodes and JSON-LD covers that case.

05Why use wp_json_encode for escaping?

Never build JSON-LD by joining strings. A title with a quote mark or a stray backslash breaks the JSON, and a value containing </script> could end the script block early. Encode a PHP array instead.

WordPress's wp_json_encode() function wraps PHP's json_encode() with extra confidence checks and returns false if the value cannot be encoded, so check for false before printing. It passes flags straight through, and PHP's JSON constants reference documents the useful ones: JSON_HEX_TAG converts every < and > to \u003C and \u003E, and JSON_UNESCAPED_SLASHES stops forward slashes being escaped, which keeps URLs readable. JSON_UNESCAPED_UNICODE encodes accented and other multibyte characters literally instead of as escape sequences, so the block stays legible when you check it by eye. Flags combine with the bitwise OR operator, as in the example.

Strip HTML from text values with wp_strip_all_tags() before encoding. Titles and excerpts can contain markup that has no place in a headline or description property, and the encoder will not remove it for you.

06How do you test hand-written markup?

Test the live URL of a post, not the code, because the point is to check what WordPress actually printed. Fetch the page with curl and confirm exactly one block appears in the raw HTML, then run the URL through the Rich Results Test. The structured data testing guide walks through both and explains how to read warnings.

If a plugin also prints markup, check that your block does not duplicate its main entity. The schema plugin audit shows how to decide which component owns which type.

07Maintenance

Hand-written markup needs a little care over time. Keep the file in version control, note in its header why it exists, and retest one URL per template after WordPress or theme updates. When Google changes a feature's required properties, update the array and retest. If you later install an SEO plugin, compare its output with your block on the first day; deciding which component owns Article markup is easier before both have been live for months.

For background on how structured data works and which types are worth adding at all, start at the handbook home. The other how-tos, from WooCommerce to testing, are in the WordPress section.

08Questions

Q1Is it safe to add JSON-LD in functions.php?

It works, but the markup then depends on the active theme and disappears if you change themes. Schema describes content rather than design, so an mu-plugin or a small regular plugin is usually a better home. Either way, encode with wp_json_encode rather than writing JSON by hand.

Q2Do I have to use wp_head, or can JSON-LD go in the footer?

Google reads JSON-LD in the head or the body, so the footer works too. wp_head is the conventional place and keeps the markup next to other metadata. What matters more is that PHP prints the block in the HTML response rather than JavaScript adding it later.

Q3Will hand-written schema conflict with my SEO plugin?

It can, if both describe the same main entity. Switch off the plugin's output for the type you hand-write, or add your data through the plugin's own filters instead. Test one URL per template afterwards to confirm a single, consistent graph.

Q4What happens if a custom field is empty?

With the pattern in this guide, nothing is printed for that property, or the block is skipped when a required value is missing. That is deliberate: incomplete items are not eligible for rich results, and placeholder values would describe content that is not on the page.

09Sources

  1. Must-Use PluginsWordPress Advanced Administration Handbook
  2. wp_head hookWordPress Developer Resources
  3. wp_json_encode() functionWordPress Developer Resources