ServicesAI Audit
← Back to Blog

How to Add JSON-LD to WordPress Without a Plugin

JSON-LDWordPress SEOSchema MarkupAI VisibilityStructured DataLLM SEO
Close-up of a vintage typewriter with a paper displaying 'WordPress', ideal for blogging and writing concepts.

Why manual JSON-LD often beats plugin-generated schema

Plugins are convenient. But convenience comes at a cost. Most WordPress schema plugins generate generic, templated markup that ticks boxes without actually telling AI search engines anything specific or useful about your business. The result is schema that passes validation but does almost nothing to improve your visibility in ChatGPT, Perplexity, or Gemini.

When you add JSON-LD manually, you control exactly what gets output. You can include your real business details, your actual product attributes, the precise questions your customers ask, and the specific claims you want AI engines to associate with your brand. That level of control matters more now than it ever did in traditional SEO.

There is also a practical argument: fewer plugins means fewer moving parts. Every plugin you install is another thing that can break during a WordPress update, conflict with your theme, or slow down your page load. If you are comfortable with a small amount of code, manual JSON-LD is genuinely the cleaner solution.

Where to place your JSON-LD in WordPress

JSON-LD schema needs to live inside a <script type="application/ld+json"> tag in the <head> section of your page, or at the very least somewhere in the document. Google and other crawlers accept it in the body too, but the head is the safest, most reliable location.

In WordPress, you have three sensible options for getting it there.

Option 1: The functions.php file

For site-wide schema, like an Organization or WebSite block that applies to every page, your theme's functions.php file is the right place. You use the wp_head action hook to inject the script tag into the head of every page.

Here is a basic example for an Organisation schema:

function flinn_add_organization_schema() {
    $schema = [
        "@context" => "https://schema.org",
        "@type" => "Organization",
        "name" => "Your Business Name",
        "url" => "https://yourdomain.com",
        "logo" => "https://yourdomain.com/logo.png",
        "contactPoint" => [
            "@type" => "ContactPoint",
            "telephone" => "+44-1234-567890",
            "contactType" => "customer service"
        ]
    ];
    echo '<script type="application/ld+json">' . json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . '</script>';
}
add_action('wp_head', 'flinn_add_organization_schema');

A few important notes here. Always use a child theme rather than editing the parent theme's functions.php directly. If you update your theme, the parent file gets overwritten and your schema disappears. A child theme is safe. Also, the JSON_UNESCAPED_SLASHES flag is worth including because PHP's default json_encode() will escape forward slashes in URLs, which makes the output harder to read and debug.

Option 2: The page or post editor (for page-specific schema)

If you want to add schema to a single post, a landing page, or a specific product page, you can paste the raw <script> block directly into the page using a Custom HTML block in the WordPress block editor. This works, but it is messy. The schema ends up in the body rather than the head, and it becomes harder to manage as your site grows.

A cleaner version of this approach is to use a custom field. Add a text area custom field to your post type, paste your JSON-LD there, and then echo it in your theme's head.php or via a wp_head hook that checks for the existence of that field. That keeps your schema out of the visible content area and makes it easy to update per page.

Option 3: A child theme's header.php

For fixed, site-wide schema that never changes, you can paste the script block directly into your child theme's header.php, just before the closing </head> tag. This is the simplest possible approach. It is also the least flexible because any change requires editing a theme file. Use this only if the schema is genuinely static.

Building your JSON-LD block from scratch

The structure of a JSON-LD block is always the same regardless of which schema type you are using. It starts with the context declaration, then the type, and then the properties specific to that type. Here is a stripped-back Article schema as an example:

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to Add JSON-LD to WordPress Without a Plugin",
  "author": {
    "@type": "Person",
    "name": "Jane Smith",
    "url": "https://yourdomain.com/about"
  },
  "datePublished": "2025-01-15",
  "dateModified": "2025-06-01",
  "publisher": {
    "@type": "Organization",
    "name": "Your Brand",
    "logo": {
      "@type": "ImageObject",
      "url": "https://yourdomain.com/logo.png"
    }
  },
  "description": "A practical guide to adding JSON-LD schema markup to WordPress without relying on a plugin."
}

The properties that matter most for AI search visibility are headline, author, datePublished, and description. These are the fields that AI engines actually use when deciding whether your content is credible and citable. Leaving them blank or filling them with placeholder text defeats the purpose entirely.

One thing worth knowing: you can include multiple schema blocks on a single page by wrapping them in a JSON array or by outputting separate <script> tags. There is no limit. A product page might sensibly carry Product, BreadcrumbList, and FAQPage schema all at once.

Conditional output: serving different schema on different page types

One of the real advantages of the manual approach is that you can use WordPress's conditional tags to serve different schema depending on what kind of page is being viewed. This is something most basic plugins do poorly.

Here is a pattern that works well:

function flinn_conditional_schema() {
    if ( is_single() ) {
        // Output Article schema
    } elseif ( is_front_page() ) {
        // Output WebSite and Organization schema
    } elseif ( is_page('contact') ) {
        // Output LocalBusiness or ContactPage schema
    }
}
add_action('wp_head', 'flinn_conditional_schema');

WordPress gives you a rich set of conditionals: is_single(), is_page(), is_category(), is_archive(), is_home(), and so on. Combined with get_post_meta() to pull in dynamic values like a post's actual title, author name, or publication date, you can build a schema system that is genuinely specific and accurate for every page on your site.

Dynamic values are important. Rather than hardcoding your post title in the schema, use get_the_title(). Rather than hardcoding a date, use get_the_date('c') which returns an ISO 8601 format that schema validators and AI crawlers expect to see.

Testing and validating your output

Once you have added your JSON-LD, always validate it before assuming it is working. Google's Rich Results Test at search.google.com/test/rich-results is the most important tool, but for AI-focused schema it is also worth using Schema.org's own validator at validator.schema.org, which checks against the full schema vocabulary rather than just the subset Google uses for rich results.

Common errors to watch for:

  • Missing required properties (the validator will tell you which ones)
  • Invalid date formats (always use ISO 8601: 2025-06-01T12:00:00+00:00)
  • Escaped slashes in URLs from PHP's default json_encode()
  • HTML entities appearing inside the JSON block (use esc_attr() carefully or strip HTML before encoding)

It is also worth using your browser's developer tools to inspect the page source and confirm the schema is actually being output in the <head> where you expect it. A function that fails silently due to a PHP error will not show any schema at all, and the validator will just return an empty result.

How this connects to AI search visibility

Adding JSON-LD to WordPress manually is not just a developer exercise. It is one of the most direct things you can do to help AI search engines understand and cite your content correctly. AI engines like ChatGPT and Perplexity parse structured data to build their understanding of what a page is about, who created it, and whether it is trustworthy.

A page with accurate Article schema that names a real author, a real organisation, a real publication date, and a specific description is far more citable than a page with no schema or with generic plugin output. This is the core argument at FlinnSchema: structured data done properly changes how AI engines see your business, not just how Googlebot does.

If your WordPress site already has Yoast or another plugin handling some schema, that does not mean you cannot add manual JSON-LD on top. The two can coexist. You might let Yoast handle breadcrumbs and add your own FAQPage or Person schema manually where the plugin falls short. Read our post on whether Yoast SEO adds the right schema for AI search visibility to understand exactly where the gaps tend to be.

If you want to understand how Article schema specifically helps get your blog posts picked up by AI, the post on using Article schema to get your blog posts cited by AI goes deeper on the specific properties that matter most.

For sites that want a proper audit before building anything out, FlinnSchema's free AI visibility audit will tell you exactly what schema is missing, what is broken, and what would make the biggest difference for your specific site.

Frequently Asked Questions

Will manually added JSON-LD conflict with schema from a plugin like Yoast?

It can, but it usually does not. The main risk is duplication: if both Yoast and your manual code output an Organization block with different values, a validator might flag it as inconsistent. The safest approach is to identify which schema types your plugin handles and only add manual JSON-LD for the types it does not cover, or where you need more precise control. If you do want to replace plugin-generated schema entirely, you can disable Yoast's schema output via a filter hook.

Do I need to know PHP to add JSON-LD to WordPress manually?

A small amount helps, but you do not need to be a developer. If you are comfortable editing a functions.php file and understand basic PHP syntax, like how arrays and echo work, you can follow the examples in this post. For truly static schema that never changes, you can even paste a raw script block into a Custom HTML block in the editor without touching any PHP at all.

How do I make the schema output dynamic so it reflects each post's actual content?

Use WordPress template tags inside your PHP function. For example, get_the_title() returns the current post's title, get_the_date('c') returns the publication date in ISO format, and get_the_author_meta('display_name') returns the author's name. These pull live data from the database, so every post gets accurate, specific schema without you having to manually update anything. Pair these with conditional tags like is_single() to make sure the function only fires on the relevant page types.

Is JSON-LD in the body valid, or does it have to be in the head?

Both locations are technically valid. Google's documentation states it will process JSON-LD in both the head and body of a page. The head is generally preferred because it is parsed early and consistently across crawlers. If you are using a Custom HTML block in the WordPress editor, the schema will land in the body, which is fine for most purposes. For production sites handling high volumes of crawling, putting schema in the head via a hook is the cleaner, more reliable approach.

Want to check your AI visibility?

Run a free audit on your website and see how visible you are to ChatGPT, Perplexity, and other AI search engines.

Run Free Audit