Adding schema markup to WordPress without a plugin
Yoast covers Article and Organization automatically. Rank Math covers most types if you pay. But if you need Product, Recipe, or JobPosting on a free WordPress install — or you just don't want another plugin — here are three approaches that work in 2026.
The plugin trap
Every popular schema plugin wants to own all schema on your site. That's by design — it's how they sell upgrades. The downside: enabling two of them produces duplicate, sometimes conflicting JSON-LD that Google has to deduplicate, which it does inconsistently.
The pragmatic answer is to pick one plugin for the schema types it does well, and do the rest by hand for the types it doesn't.
Approach 1: functions.php with a wp_head hook
The most direct method. Add to your theme's functions.php (or, better, a child theme):
add_action('wp_head', 'devtoolbox_inject_recipe_schema');
function devtoolbox_inject_recipe_schema() {
if (!is_singular('recipe')) return;
global $post;
$name = get_the_title($post);
$image = get_the_post_thumbnail_url($post, 'full');
$ingredients = get_post_meta($post->ID, 'ingredients', true); // array
$steps = get_post_meta($post->ID, 'steps', true); // array
if (empty($ingredients) || empty($steps)) return;
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Recipe',
'name' => $name,
'image' => $image,
'recipeIngredient' => $ingredients,
'recipeInstructions' => array_map(
fn($step) => ['@type' => 'HowToStep', 'text' => $step],
$steps
),
];
echo '<script type="application/ld+json">'
. wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. '</script>';
}
Three things worth flagging:
- Always early-return if the data isn't there. Half-filled schema is worse than no schema.
- Use
wp_json_encode, notjson_encode. WordPress's wrapper handles a couple of edge cases around UTF-8 that the PHP built-in does not. - Guard with
is_singular('recipe')or your post type, otherwise the schema renders on every page including the home page, where it doesn't make sense.
Approach 2: Custom block in the editor
If you only need schema on a handful of pages, the Gutenberg HTML block is the lowest-effort path. Edit a post, add a Custom HTML block, paste:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Is the trial free?",
"acceptedAnswer": { "@type": "Answer", "text": "Yes, 14 days." }
}
]
}
</script>
This works for any schema type and is invisible to readers. The downside: it's manual and easy to forget when editing. Use it for one-offs, not for templated content.
Approach 3: Child theme template override
For content that ships through a custom template — say, a single-event.php for an Events custom post type — you can put the JSON-LD directly in the template:
<head>
<?php wp_head(); ?>
<?php
$event_id = get_the_ID();
$start = get_post_meta($event_id, 'start_date', true);
if ($start):
?>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Event",
"name": <?= wp_json_encode(get_the_title()); ?>,
"startDate": <?= wp_json_encode($start); ?>
}
</script>
<?php endif; ?>
</head>
This couples the schema to the rendering, so the data shown on the page and the data in the schema can never drift apart. That's the cleanest pattern for content with strict shape.
When to give up and use a plugin
If you find yourself maintaining schema for 4+ post types, with variants and conditional fields, the custom-code path will turn into your second job. Rank Math's schema engine handles most types automatically — if you don't want to maintain custom code, Rank Math is the path of least resistance.
Verifying it works
The non-negotiable last step: View Source on a real published page and confirm the <script type="application/ld+json"> is there. Inspect Element will show client-injected schema; View Source shows what the crawler sees. The two often differ.
Then paste the rendered URL into the URL validator on the homepage — it parses the page exactly the way Google does and tells you which fields would qualify.
Generate your schema with the Recipe builder or Product builder, then paste the <script> block into the WordPress HTML block.