PHP Schema Markup: Build Advanced JSON-LD in 7 Steps

Santaji GadeDevelopmentPHPYesterday4 Views

PHP Schema Markup

Generate php and schema markup dynamically with a reusable PHP class covering nested Article and FAQ schema, safe output escaping, caching, and validation.

Development PHP Schema Markup JSON-LD

A static JSON-LD block pasted into a template describes one page and stays wrong the moment that page's content changes. PHP and schema markup generated at request time from the same data that renders the page never drifts out of sync with what a visitor actually sees.

01

Why PHP Schema Markup Beats a Hardcoded JSON-LD Block

A hand written JSON-LD block works for exactly one page, on exactly one day. The author changes, a price updates, a new FAQ gets added, and the markup silently stops matching reality.

Search engines cross check structured data against the visible content of the page, and a mismatch between the two is treated as a spam signal, not a harmless typo. The vocabulary itself comes from schema.org, a shared standard maintained jointly by the major search engines.

Generating the same JSON-LD from the database row or content object already powering the page template removes that entire class of drift by construction.

The rest of this guide builds a small, reusable PHP class that composes multiple schema types from real content and outputs a validated block on every request.

02

Designing a Schema Builder Class

A single class with a fluent interface keeps every schema type consistent and makes it easy to add new types later without touching existing page templates. This is the foundation every other piece of php and schema markup in this guide builds on top of.

SchemaBuilder.php
final class SchemaBuilder {
    private array $nodes = [];

    public function addNode(array $node): self {
        if (empty($node['@type'])) {
            throw new InvalidArgumentException('Every schema node needs an @type.');
        }
        $this->nodes[] = $node;
        return $this;
    }

    public function toArray(): array {
        if (count($this->nodes) === 1) {
            return array_merge(['@context' => 'https://schema.org'], $this->nodes[0]);
        }
        return [
            '@context' => 'https://schema.org',
            '@graph' => $this->nodes
        ];
    }
}

Throwing on a missing @type catches a broken node during development instead of shipping a silently invalid block to production.

03

Generating Article Schema From Dynamic Content

An Article node pulls its fields directly from the same content object the page template already uses to render the headline, body, and byline.

build_article_node.php
function build_article_node(array $post): array {
    return [
        '@type' => 'Article',
        'headline' => $post['title'],
        'datePublished' => (new DateTimeImmutable($post['published_at']))->format(DateTimeInterface::ATOM),
        'dateModified' => (new DateTimeImmutable($post['updated_at']))->format(DateTimeInterface::ATOM),
        'mainEntityOfPage' => [
            '@type' => 'WebPage',
            '@id' => $post['canonical_url']
        ],
        'image' => [
            '@type' => 'ImageObject',
            'url' => $post['featured_image'],
            'width' => 1200,
            'height' => 675
        ]
    ];
}

DateTimeImmutable combined with the ATOM format constant guarantees a timezone correct ISO 8601 string, which schema validators check strictly.

04

Adding Nested Author and Publisher Objects

Author and publisher information is itself structured data, nested inside the Article node rather than flattened into simple strings.

add_author_publisher.php
$article['author'] = [
    '@type' => 'Person',
    'name' => $post['author_name'],
    'sameAs' => $post['author_social_links']
];

$article['publisher'] = [
    '@type' => 'Organization',
    'name' => 'Brandella Journal',
    'logo' => [
        '@type' => 'ImageObject',
        'url' => 'https://brandella.in/logo.png',
        'width' => 600,
        'height' => 60
    ]
];

sameAs expects an array of profile URLs, and it is one of the few fields where a plain array of strings is correct instead of another nested object. For a larger project, a typed package like spatie/schema-org can replace hand written arrays with fluent builder objects for every schema.org type.

05

Combining Multiple Types With an @graph Array

A typical article page carries more than one schema type at once, Article, BreadcrumbList, and often FAQPage, and Google's own guidance recommends combining related nodes into a single graph rather than emitting several separate script tags. The official JSON-LD specification covers the full syntax this pattern relies on.

build_page_schema.php
$schema = new SchemaBuilder();
$schema->addNode(build_article_node($post));
$schema->addNode(build_breadcrumb_node($post['category'], $post['title']));

if (count($post['faqs']) >= 2) {
    $schema->addNode(build_faq_node($post['faqs']));
}

Each addNode call stays independent, so a page missing FAQ content simply skips that node instead of shipping an empty or malformed FAQPage block.

FormatLives InCommon Use
JSON-LDA separate script tag, detached from HTMLRecommended by Google for nearly all cases
MicrodataInline itemprop attributes on HTML tagsLegacy CMS templates, harder to maintain
RDFaInline vocab and property attributesLess common outside specific publishing systems
06

Generating FAQPage Schema Conditionally

FAQPage schema only makes sense when the page actually renders visible question and answer content, since structured data must reflect what a visitor can read.

build_faq_node.php
function build_faq_node(array $faqs): array {
    return [
        '@type' => 'FAQPage',
        'mainEntity' => array_map(fn($faq) => [
            '@type' => 'Question',
            'name' => $faq['question'],
            'acceptedAnswer' => [
                '@type' => 'Answer',
                'text' => $faq['answer']
            ]
        ], $faqs)
    ];
}

array_map with an arrow function keeps the transformation from a flat FAQ list into nested Question and Answer nodes in a single readable expression.

07

Escaping Output Safely for the Script Tag

JSON-LD sits inside a script tag, and content pulled from a database can contain characters that would otherwise break out of that tag if encoded carelessly.

render_schema.php
function render_schema(array $data): string {
    $json = json_encode(
        $data,
        JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
    );
    // str_replace guards against a literal closing tag ending the script block early
    $json = str_replace(', '<\/', $json);
    return sprintf('', $json);
}

JSON_THROW_ON_ERROR turns a silent encoding failure into a catchable exception, which matters since a page that fails to encode its own author name should never ship broken markup unnoticed.

08

Caching Generated JSON-LD for Performance

Rebuilding the same schema graph from scratch on every request adds work a busy page does not need to repeat between content updates.

cached_schema.php
function get_cached_schema(array $post): string {
    $key = 'schema_' . $post['id'] . '_' . $post['updated_at'];

    if (apcu_exists($key)) {
        return apcu_fetch($key);
    }

    $schema = new SchemaBuilder();
    $schema->addNode(build_article_node($post));
    $html = render_schema($schema->toArray());

    apcu_store($key, $html, 3600);
    return $html;
}

Building the cache key from updated_at means an edited post automatically produces a new key and a fresh block, with no separate cache invalidation step to remember.

  • Never hand write a field the page already computes: pull every value from the same data the template renders, so schema and content cannot drift apart.
  • Throw on invalid nodes early: a missing required property should fail loudly during development, not silently in production.
  • Escape closing tags in the JSON string: a stray value containing a literal closing script tag can otherwise break the page.
  • Key the cache off an updated timestamp: this keeps stale schema from ever outliving the content it describes.
  • Test with the Rich Results Test before shipping: a syntactically valid JSON-LD block can still fail schema.org's own required field rules.
09

Validating Schema Before It Ships

A quick automated check in a build step or test suite catches a broken schema block before it ever reaches a real page, the final safeguard around every part of php and schema markup covered so far.

validate_schema.php
function assert_valid_schema(string $json): void {
    $decoded = json_decode($json, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new RuntimeException('Invalid JSON in schema block: ' . json_last_error_msg());
    }
    if (empty($decoded['@context'])) {
        throw new RuntimeException('Schema block is missing @context.');
    }
}

Running this against every generated page during a deploy pipeline turns a schema regression into a failed build instead of a silent ranking problem discovered weeks later. Google's own Rich Results Test and schema.org's own Schema Markup Validator are both still worth running by hand before a major template change goes live.

10

Watch: Understanding Structured Data

Google Search Central's own explainer covers how structured data is actually used once it reaches Google, which is useful context for why the validation step above matters.

Video credit: Google Search Central.

Everything in this guide exists to make sure the structured data described in the video is actually correct, current, and safe to output on every page.

Content Data Schema Builder Escape and Validate APCu Cache JSON-LD Output

Frequently Asked Questions

A plugin works well for simple sites, but a custom class gives full control over nested types, conditional nodes, and caching, which matters more as a site's content structure grows complex.

No, but it is recommended. Multiple separate script tags still work, an @graph array just keeps related nodes explicitly connected and easier for a parser to associate correctly.

Because dynamic content can contain a literal closing script tag, which would otherwise end the JSON-LD block early and break the rest of the page's HTML.

No. Valid schema makes a page eligible for a rich result, but Google decides case by case whether to actually display one, based on quality and relevance signals.

After any change to the builder class or the underlying content fields, since a small field rename can silently break a node without throwing an error anywhere obvious.

What We Learn Today

1

Static schema drifts from content

Php and schema markup generated from live data prevents that mismatch entirely.

2

Nested objects model real relationships

Author, publisher, and image belong as objects, not flat strings.

3

@graph groups related types

One block can carry Article, BreadcrumbList, and FAQPage together.

4

Output escaping is not optional

A stray closing tag in dynamic content can break the whole page.

5

Caching needs a content aware key

Keying off updated_at keeps cached schema from ever going stale.

6

Validation belongs in the build step

Catching a broken node before deploy beats finding it in Search Console.

Ready to Generate Schema Dynamically?

Pair php and schema markup with a wider technical SEO toolkit to keep every rich result eligible page accurate and current.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...