What is schema markup, and is it still relevant?
Schema markup is structured data written in the schema.org vocabulary. Google calls structured data a standardized format for providing information about a page and classifying its content, and it uses what it finds to understand the page. On WordPress that's a JSON-LD block saying who publishes the site, what the page is and, for a business, where it is. Most of what I do as a schema markup consultant is making that block and the visible page say the same thing.
Is it still relevant? Yes, for narrower reasons than many plugin pages suggest. Google says it doesn't guarantee structured data will show up in results, even when the Rich Results Test passes it, and some features have gone: its changelog says the FAQ rich result stopped appearing in Search from 7 May 2026. Breadcrumbs, product snippets, merchant listings and review snippets are still in Google's list of supported features, and they all need the markup.
What is the best schema plugin for WordPress?
Usually the one that's already installed. Most WordPress sites run Yoast or Rank Math, and both print structured data before anyone touches a setting. Yoast builds one @graph of pieces joined by @id, and its documentation says every piece can be altered, extended or switched off through its API. Rank Math adds schema through a Schema Generator in the post editor. Here's which SEO plugin I'd pick.
A dedicated schema plugin earns its place when the SEO plugin can't express a type. Schema & Structured Data for WP & AMP lists more than 35 schema types on its plugin page. Schema Pro maps page fields such as title, author and date into markup, and says it works alongside Yoast, with settings that decide which plugin controls what. That setting is the whole question: if both plugins print an Organization, the page describes the business twice, so I switch the duplicate off first.
| What the site needs | What I use | Why |
|---|---|---|
| Organization or Person, WebSite, WebPage, breadcrumbs and articles | The SEO plugin's own settings | It already prints these, linked, on every template |
| A specific business type with address, hours and area served | The plugin's local settings, or its filter when the type isn't offered | The business stays one entity inside the existing graph |
| A type the plugin can't express at all | One JSON-LD block from a child theme or site plugin, reusing the same @id | It survives a theme update and doesn't describe the business twice |
| A dedicated schema plugin on top of the SEO plugin | Only for types the SEO plugin can't print | Two plugins tend to mean two graphs for one business |
WooCommerce is its own case. It prints Product markup on product pages with no SEO plugin involved: name, URL, description, image, SKU, a GTIN when a valid one's entered, and an Offer with price, currency, availability and a priceValidUntil date. Ratings and the five most recent reviews appear only when the product has them. It doesn't print a brand, shipping details or a return policy; those go in through the woocommerce_structured_data_product filter. For stores already seeing warnings, here's how I fix Product snippet errors without fake reviews.
Adding schema in Elementor follows the same rule. The builder lays out the visible page and the SEO plugin prints the graph, so I don't paste JSON-LD into an HTML widget on each page, where nobody finds it later.
Which schema types does a business site need?
Fewer than most plugins offer. A business needs its identity said once, then each page described as what it is.
| Type | What it tells Google | Where it goes |
|---|---|---|
| Organization, or Person for a one-person brand | Who publishes the site: name, logo and profiles | Once, sitewide, with one @id |
| WebSite | The site itself, published by that entity | Once, sitewide |
| WebPage and BreadcrumbList | What this URL is and where it sits | Every page, matching the visible breadcrumb |
| A LocalBusiness subtype | The business type, address, hours and area served | Where those facts are shown, usually home and contact |
| Article or BlogPosting | Who wrote a post and when | Posts only |
Google's local business documentation asks for the most specific subtype possible, so a fireplace company isn't just LocalBusiness and a jeweller isn't just a Store. On GK Jewelers' store, the homepage declares a JewelryStore with its postal address, plus WebSite and SearchAction markup.
Alpenglow Fireplace, a fireplace and chimney site, went live with HVACBusiness markup declaring its postal address, geo coordinates, opening hours and the city it serves. That shows what was built, not that the markup caused anything in Search Console. Schema is one layer of the website side of local search, and it only helps when the pages and the Business Profile say the same thing.
Rather have me do this? One person, whole job.
Book a call on UpworkHow I add schema to a WordPress site, step by step
Six moves. Old sites can carry markup from plugins they stopped using years ago, and some themes print their own, so I read what's there first, even on a fresh install.
- Step 01
Read what the page prints today
I run the homepage, a service page and a post through the Rich Results Test and read the source for every
application/ld+jsonblock, noting which plugin printed each one. If two blocks already describe the business, that gets fixed first. - Step 02
Copy the facts from the page, not from memory
Name, logo, address, phone, hours and towns served get written down exactly as the page and the Business Profile show them. If those two disagree, the markup waits. Each fact then goes on the page that shows it.
- Step 03
Pick the most specific type
A fireplace service became HVACBusiness, not LocalBusiness. If nothing fits closely, I stay with the nearest honest parent type rather than stretch one that doesn't.
- Step 04
Set the entity once in the SEO plugin
In Yoast that's the Organization or Person setting. In Rank Math it's Titles and Meta, Local SEO, once the Local SEO module's switched on. Every template then inherits the same entity.
- Step 05
Give every piece a stable @id
The business gets one identifier, like
https://example.com/#organization, and every other piece points at it. WebSite names it as the publisher, and WebPage says it's part of the WebSite. That turns a pile of blocks into one described business. - Step 06
Add only what the plugin can't
A missing property goes in through the plugin's filter. A missing type goes in as one JSON-LD block that reuses the same
@id, printed the way the next section shows.
Don't mark up what the page doesn't show. Google's general guidelines say not to mark up content that isn't visible to readers of the page, so hours in the markup but missing from the page break the rule quietly, even when they're right.
How do I add schema to WordPress without a plugin?
Put the code where theme updates can't reach it: a child theme's functions.php, a small site plugin, or a code snippets plugin such as WPCode if you'd prefer not to touch files. WordPress's theme handbook warns that a function added to the parent theme's functions.php will disappear the next time the theme is updated. Here's the pattern I use on a contact page.
- Step 01
Hook into wp_head
WordPress's
wp_headhook prints scripts or data in the head tag on the front end, which is where the block belongs:add_action( 'wp_head', 'site_contact_schema' ); - Step 02
Print it only where the facts are shown
The function returns early unless
is_page( 'contact' )is true, so the hours and address are only claimed on the page that shows them. - Step 03
Build the data as a PHP array
$data = array( '@context' => 'https://schema.org', '@type' => 'HVACBusiness', '@id' => 'https://example.com/#organization', 'name' => 'Example Fireplace', 'openingHours' => 'Mo-Fr 08:00-17:00', 'areaServed' => 'Calgary' );The@idis the one the SEO plugin already uses, so this adds facts to that business rather than creating a second one. - Step 04
Encode it and print it
echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>';I let WordPress's ownwp_json_encode()write the JSON rather than typing it by hand, so a quote mark in a business name can't break the block. - Step 05
Pull page facts from fields, not the code
When each location's hours live in Advanced Custom Fields, the array reads them with
get_field(), which returns the value of a specific field. The markup then changes whenever someone edits the field, and nobody has to find the code.
Check the live page source afterwards for exactly one block with that @id. Two blocks naming the same business differently is the mess I'm most often asked to untangle.
How do I test it before and after publishing?
Two tools check two different things, and a pass in one says nothing about the other. After launch, Search Console tells me whether it's holding up.
- i.
Rich Results Test, on the code
Before anything ships I paste the snippet into the Rich Results Test, which accepts code as well as a URL and flags missing required properties.
- ii.
Schema Markup Validator, for the vocabulary
It checks for valid schema.org properties, including types Google doesn't consume, so it catches a misspelt property on HVACBusiness that the Rich Results Test ignores.
- iii.
Rich Results Test, on the live URL
After publishing I test the real URL, because the rendered page is what Google reads, and a caching plugin or theme update can change the output.
- iv.
URL Inspection in Search Console
Google points to URL Inspection to confirm it's actually discovered the markup on a page, which is a different thing from the markup existing.
- v.
Rich result reports over the next weeks
Google warns that markup can break after deployment through templating or serving issues. The rich result reports show errors across every URL of a type.
A clean test means a page is eligible, not that anything will show, so I never sell markup as a listing upgrade.
Why won't I add review stars that aren't earned?
Because a business can't award them to itself. Google's review snippet guidelines say that when an entity controls the reviews about itself, its pages using LocalBusiness or any other Organization type aren't eligible for star reviews. Testimonials a business chose and published on its own site are exactly that.
Marked-up reviews have to be readily available on the marked-up page. An AggregateRating copied from a Business Profile into the homepage code fails both tests.
It isn't a harmless gamble. Google's structured data guidelines say violations can lead to a manual action that takes pages out of the running for rich results, which throws away the only thing the markup was for. Where reviews are real, visible and about something other than the business, like a product a store sells, I'll mark them up exactly as the page shows them.
If you'd like one person to read what your pages print, set the entity once and test it on the live URLs, that's the schema work I do, remotely. Here's how a project runs from the first call.
Book a call on Upwork
