Développeur Drupal vérifiant la sortie JSON-LD dans les outils de développement du navigateur à côté d'un formulaire d'édition produit - métaphore du markup Schema.org piloté par les champs avec Metatag.

JSON-LD in Drupal: how to generate structured data from fields with Schema.org Metatag

The safest way to add JSON-LD to Drupal is to map Schema.org properties to existing content fields. Configure the mapping once per content type with Metatag and Schema.org Metatag. Editors then update the product, article or service as usual, while Drupal generates the JSON-LD from the same values shown on the page.

JSON-LD in Drupal works best when structured data comes from the same field model that supplies the visible page. That pattern avoids a second, hidden copy of prices, names and availability. It also gives a development team configuration to export, review and test. Read also: Schema.org and metadata in Drupal - the broader picture before you map individual bundles with Schema.org Metatag.

This is better than asking editors to maintain a second, hidden copy of the facts. The setup is straightforward. The hard part is deciding which fields are authoritative, handling empty values and making sure a new content type does not reach production without a mapping. Technical SEO also depends on markup that matches what visitors see, not only on syntax that passes a validator.

In this article:

Why does handwritten JSON-LD drift from page content?

Suppose a product page shows a price of EUR 249. The page template reads that value from field_price, but someone entered the same number manually in a JSON-LD block.

The commercial team changes the price to EUR 269. The visible page updates. The hidden markup does not.

Now the page makes two claims about the same product. Syntax validation may still pass because both values are valid numbers. The problem is accuracy.

Google's structured data guidelines say that markup should describe content visible to the reader. A product name, price or availability in JSON-LD should therefore come from the same source as the visible value. The same rule applies when text in images hides facts from search and AI fetchers - if the fact is not in HTML, structured data cannot reliably stand in for it.

Drupal already has that source: fields.

Schema.org Metatag extends the Metatag module. It defines groups for types such as Article, Product, Service, Organization and WebSite, then renders the configured properties as an application/ld+json script in the page head. Tokens connect those properties to entity fields.

One edit can update both outputs.

Which Schema.org Metatag submodules should you enable?

Schema.org Metatag 3.0.4 supports Drupal 9, 10 and 11. The base module requires Metatag and PHP 8 or newer. Install the package with Composer:

composer require 'drupal/schema_metatag:^3.0'

Enable the base module and the submodules that match the pages you will mark up. For a product catalogue:

drush en schema_metatag schema_product schema_organization schema_web_site -y
drush cr

Do not enable every submodule automatically. Each one adds another group of settings and another type the team may feel obliged to configure. Start with the pages that contain useful, maintained data.

A sensible first pass is:

  • schema_web_site and schema_organization for site-wide identity,
  • schema_article for articles and news,
  • schema_product for real product pages,
  • schema_service for service pages when the properties describe visible content,
  • the module's breadcrumb support where the site already renders breadcrumbs.

Schema.org contains far more types than the module exposes. Google also supports rich results for a smaller set than Schema.org defines. Choose a type because it describes the page and has a consumer, not because it exists in a list.

How do you configure Metatag defaults before mapping properties?

Go to:

Configuration → Search and metadata → Metatag

The path is:

/admin/config/search/metatag

Metatag combines values through a hierarchy:

  1. Global defaults apply across the site.
  2. Entity defaults apply to an entity type such as Content.
  3. Bundle defaults override them for a content type such as Product.
  4. Entity-level values can override the defaults when a Metatag field is attached to an individual record.

The bundle level should do most of the work. A Product content type needs one reviewed mapping that all product nodes inherit.

Avoid entity-level schema editing unless the page is a genuine exception. It asks editors to maintain technical metadata they cannot see in the normal page design. It also makes two products of the same content type behave differently.

Global defaults suit identity that does not change per page, such as the publisher organization. Bundle defaults suit properties connected to fields, such as a product name, SKU and description. The same principle applies when you teach editors to work in components instead of one long body field - one authoritative source, many outputs.

How do you map Drupal fields to Product JSON-LD?

Assume the Drupal content type contains these fields:

Drupal sourceField typeVisible use
Node titleCore titleproduct heading
field_summaryPlain text, longintroduction
field_skuPlain textspecification table
field_brandEntity referenceproduct details
field_imageMedia or image referenceproduct image
field_priceDecimaloffer block
field_currencyListoffer block
field_availabilityListstock status
Canonical URLGenerated URLpage identity

The Schema.org mapping should read those same values:

Schema.org propertyDrupal sourceTypical token or setting
Product @typefixed configurationProduct
Product @idcanonical URL plus fragment[node:url:absolute]#product
Product namenode title[node:title]
Product descriptionsummary field[node:field_summary]
Product urlcanonical URL[node:url:absolute]
Product skuSKU field[node:field_sku]
Product brand.namereferenced brand nametoken for field_brand label
Product imageimage or media fieldabsolute image URL token
Offer pricedecimal price fieldtoken for field_price raw value
Offer priceCurrencycurrency list valuetoken for field_currency stored value
Offer availabilityavailability list valuefull Schema.org status URL

Token paths for referenced entities and media depend on the field structure and enabled token providers. Use Browse available tokens in the Metatag form and inspect the result on a real page. Do not copy a media token from another project and assume its relationship chain is identical.

The core mapping principle is stable:

  • names come from name fields,
  • identifiers come from identifier fields,
  • numbers come from numeric fields,
  • controlled statuses come from stored machine values,
  • URLs must resolve to absolute public URLs,
  • nested objects such as Offer and Brand should keep their own @type.

Field-driven JSON-LD also supports editorial patterns such as answer-first writing for AI search, where visible copy and metadata must describe the same facts.

Configure the Product defaults

At /admin/config/search/metatag, add or edit the defaults for the Product content type. Open the Schema.org: Product section and set:

  • @type to Product,
  • name to the node title token,
  • description to the summary token,
  • url to the absolute node URL,
  • sku to the SKU field,
  • image to the public image URL,
  • brand as a Brand object,
  • offers as an Offer object populated from the price fields.

The module's form represents complex properties as nested inputs. Use those controls instead of assembling a JSON string in a text field.

Save the defaults, clear caches and open a published product page. The head should contain one JSON-LD script with a Product object in its @graph.

The result should follow this shape:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Product",
      "@id": "https://www.example.com/products/aqua-4500#product",
      "name": "Aqua 4500",
      "description": "A submersible pump for continuous industrial use.",
      "url": "https://www.example.com/products/aqua-4500",
      "sku": "AQ-4500",
      "brand": {
        "@type": "Brand",
        "name": "AquaWorks"
      },
      "offers": {
        "@type": "Offer",
        "price": "269.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    }
  ]
}

This output is an example, not a block to paste into Drupal. Your mapping should generate it from the product entity.

How do you export Schema.org Metatag mapping as Drupal config?

Once the page output is correct, export Drupal configuration:

drush cex -y
git diff -- config/sync

Bundle defaults are stored as Metatag default configuration entities. A Product mapping normally appears in a file named:

metatag.metatag_defaults.node__product.yml

The simple part of an exported file has this shape:

id: node__product
label: 'Content: Product'
tags:
  schema_product_type: Product
  schema_product_name: '[node:title]'
  schema_product_description: '[node:field_summary]'
  schema_product_id: '[node:url:absolute]#product'
  schema_product_url: '[node:url:absolute]'
  schema_product_sku: '[node:field_sku]'

Complex values such as Offer are serialized by the module and can be harder to review in raw YAML. Configure them through the UI, export them and commit the result. Avoid hand-editing the serialized value unless you also test the rendered page.

Configuration export matters because a mapping is application behavior. It should pass through code review and deployment with the content type and fields it depends on.

What happens when price fields are empty in JSON-LD?

Price mappings expose content-model problems quickly.

The price should come from a decimal field rather than a formatted sentence such as "From €269." Currency should have its own controlled value. Availability should store a machine value that maps to a valid Schema.org status.

Decide what happens when no public price exists. Do not emit:

{
  "@type": "Offer",
  "price": "",
  "priceCurrency": "EUR"
}

An incomplete Offer can be invalid or misleading. Either require the fields for products that publish an Offer, or omit the Offer when the business does not publish a price.

This is where a simple token mapping may need a small custom module. A hook_metatags_alter() implementation can remove a nested object when its required source fields are empty. Keep the rule close to the content model and cover it with a test.

The same rule applies to ratings, dates and identifiers. An empty token is not useful structured data. A fallback should have business meaning, not merely silence a validator.

How do you keep multilingual JSON-LD aligned with translated pages?

Tokens resolve in the context of the entity being rendered. On a translated URL, the translated title and description should feed the markup for that language.

Check this on production-like pages. Referenced entities, media metadata and custom tokens do not always follow translation in the way the main node fields do.

Classify properties before configuring them:

  • Translatable: name, description and audience-facing labels.
  • Usually shared: SKU, GTIN and technical identifiers.
  • Market-specific: price, currency, availability and area served.
  • Site-wide: publisher identity and official profiles.

A French product page should not emit an English description because the French translation is empty. Decide whether publication should be blocked, the property should be omitted or a documented fallback language should appear visibly on the page as well.

Test each language URL separately. Passing validation in English says nothing about the German price or French image metadata.

How do you validate JSON-LD on rendered Drupal pages?

Saving a token in Drupal does not prove the resulting JSON-LD is correct. Validators only help when the markup on the live page matches the fields editors maintain.

Use three levels of validation.

1. Parse the output

Open the page source and find:

<script type="application/ld+json">

Confirm that the block is valid JSON, that the expected type exists and that only one module owns each entity. Duplicate Product objects from Schema.org Metatag and a theme template can disagree without producing a syntax error.

2. Check the vocabulary

Use the Schema.org validator to inspect Schema.org types and properties.

This catches malformed nesting, misspelled properties and invalid values. It does not prove that Google uses the type as a rich result. For CMS-level SEO checks beyond markup, see 10 SEO features a modern CMS should have and top improvements after a Drupal SEO audit.

3. Check the intended Google feature

Use Google's Rich Results Test for types supported by Google Search. Then check the feature-specific documentation for required properties.

Valid Schema.org markup and rich-result eligibility are different tests. A valid Service object may describe the page well even when Google does not provide a Service rich result.

After the technical checks, compare the result with visible content. Validators cannot decide whether EUR 269 in the markup conflicts with EUR 249 in the offer block. Can an AI actually read your website? shows why fetchable HTML and aligned facts matter beyond traditional rich results.

How do you test Schema.org coverage in CI?

A team can configure every existing content type correctly and still lose coverage six months later. Someone adds a new Product variant or Resource bundle and does not add Metatag defaults.

The simplest CI check asserts that selected public bundles have the expected schema group in exported configuration.

<?php

declare(strict_types=1);

namespace Drupal\Tests\site_schema\Unit;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Yaml;

final class SchemaDefaultsConfigTest extends TestCase {

  public function testPublicBundlesHaveSchemaDefaults(): void {
    $required = [
      'article' => ['schema_article_type', 'Article'],
      'product' => ['schema_product_type', 'Product'],
      'service' => ['schema_service_type', 'Service'],
    ];

    $configDirectory = dirname(DRUPAL_ROOT) . '/config/sync';

    foreach ($required as $bundle => [$tag, $expectedType]) {
      $path = sprintf(
        '%s/metatag.metatag_defaults.node__%s.yml',
        $configDirectory,
        $bundle,
      );

      self::assertFileExists(
        $path,
        sprintf('The %s bundle has no Metatag defaults.', $bundle),
      );

      $config = Yaml::parseFile($path);

      self::assertSame(
        $expectedType,
        $config['tags'][$tag] ?? null,
        sprintf('The %s bundle has no %s mapping.', $bundle, $tag),
      );
    }
  }

}

Adjust the configuration directory and bundle list to the project. Run the test with the same PHPUnit job that checks custom Drupal code.

This test catches absence. It does not catch an incorrect price token.

Add one functional test per high-value template. Create or load a representative product, request its public URL, decode the application/ld+json script and assert the business-critical values:

  • @type equals Product,
  • name equals the node title,
  • sku equals field_sku,
  • offers.price equals the raw price field,
  • offers.priceCurrency equals the stored currency,
  • the visible price equals the generated price.

The Schema.org Metatag module uses the same approach in its own functional tests: load a rendered page, find the JSON-LD script, decode it and compare the output. Why Drupal works for structured content operations at scale shows how configuration, fields and governance keep those checks useful as the site grows.

How do you build an editorial queue for structured data review?

Bundle defaults should cover normal pages. Human review should focus on missing or exceptional source data.

Create an administrative View for Product content with columns for:

  • title,
  • publication status,
  • SKU,
  • price,
  • currency,
  • availability,
  • image,
  • translation status,
  • last changed date,
  • "Schema review required" flag.

Add exposed filters for empty required fields and the review flag. Give the View a clear name such as Structured data review queue.

The boolean flag is useful when one record cannot follow the bundle mapping, for example a product with several independent offers or a page representing a product family rather than one SKU. That record needs a modeling decision, not a manual JSON-LD text area.

If editors can enter entity-level Metatag overrides, include those records in a separate audit. Overrides are exceptions and should remain rare. When specs must also support AI-assisted buyer research, getting your company recommended by AI: supplier shortlist facts explains why published field values matter beyond search snippets alone.

How do you add JSON-LD mapping to the content-type workflow?

Adding a public content type changes more than the editing form. Use a short definition-of-done checklist:

  1. Choose the page's primary Schema.org type.
  2. Identify the fields that supply its useful properties.
  3. Configure bundle defaults with tokens.
  4. Export and review the configuration.
  5. Test one complete record and one record with optional fields missing.
  6. Test every published language.
  7. Add the bundle to the configuration coverage test.
  8. Add required source fields to the editorial review View.
  9. Confirm that the theme or another module does not generate a duplicate entity.

Repeat the review when a field changes name, type or ownership. A mapping can remain present while pointing to a token that now resolves to nothing.

Want JSON-LD mappings that stay aligned with your Drupal content?

We implement Schema.org Metatag configuration, field-to-token mappings and automated checks on Drupal platforms where product pages, articles and multilingual content must stay aligned between visible copy and structured data markup. The same pattern supports B2B sites that publish structured specs for buyer research and AI-assisted shortlists.

If your Drupal website needs maintained JSON-LD from content fields rather than hand-written blocks, our team can configure bundle defaults, export them as code and connect validation to your editorial workflow. Visit our Drupal development services page to see how we build and maintain Drupal platforms for content-heavy organizations.