Add Schema.org structured data to Drupal nodes
Schema.org structured data gives Drupal nodes a machine-readable layer that search engines and other software can interpret. It can describe an article, event, job, product, or local business using a common vocabulary instead of relying on guesses from headings and visible prose. In Drupal, this is especially useful because editorial fields already separate content, relationships, media, and publication metadata.
The most maintainable implementation maps those fields to JSON-LD, then attaches one script to the relevant full-page node view. Editors continue to work with normal Drupal forms, while the template, service, or module converts current values into valid schema properties. This avoids hand-coded HTML in articles and keeps visible content and machine-readable data synchronised.
| Markup format | Placement | Drupal fit | Main limitation |
|---|---|---|---|
| JSON-LD | A script element | Keeps metadata separate from node markup and supports reusable templates | Requires a renderer or module capable of emitting JSON |
| Microdata | HTML attributes | Works with tightly controlled theme templates | Adds repeated attributes to many elements |
| RDFa | HTML attributes | Supports rich semantic relationships | Less convenient to validate and maintain |
Understand the schema and markup format
Schema.org is a vocabulary, not a search-engine ranking factor by itself. Valid structured data can make a result eligible for a richer search appearance, but eligibility also depends on the chosen type and its required properties. Marking a generic article as a recipe merely because both have a title would create misleading data rather than a useful result.
JSON-LD is usually the cleanest option for Drupal. It keeps the description in one JSON object and leaves the theme’s HTML mostly untouched. A minimal article object might look like this:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Drupal accessibility checklist",
"datePublished": "2025-03-08T09:30:00+11:00",
"author": {
"@type": "Person",
"name": "Priya Kumar"
}
}
The @context identifies the vocabulary, while @type selects the entity being described. Use an accurate type for every node bundle: Article for editorial stories, Event for a conference, JobPosting for a vacancy, and LocalBusiness for a company location. WebPage suits general information, but it is not a catch-all for specialised content.
Check each required and recommended property against the relevant Schema.org definition. Headline, author, dates, images, and identifiers have distinct meanings and should come from matching Drupal fields. If a property does not exist in the content model, omit it rather than inventing a value.
Map Drupal content to schema properties
Start with the content model, not with a JSON template. List the fields on each node bundle, identify which values are genuinely machine-readable, and record their source. A configuration mapping can pair the article bundle with Article, then connect title to headline, field_summary to description, and field_hero_image to image.
Canonical URLs should come from the node route, while creation and modification times should come from entity timestamps. The author name comes from the owning account unless the content model deliberately supports a byline or guest author; an Article publisher should use the site name and stable logo URL. Convert body HTML to readable text when a property requires plain text, and never expose unpublished or restricted field values.
References to organisations, people, places, and media can become nested entities. Give each entity a stable @id, such as the organisation’s canonical page, so repeated information remains consistent. Avoid treating every taxonomy term as a keyword: a category can use articleSection, while a service, product, or event should use the property that accurately describes it.
Add JSON-LD without duplicating node logic
A contributed module can reduce custom code. Metatag and similar packages can be evaluated, but check Drupal 10 or 11 compatibility, maintenance status, and schema support before selecting one. Confirm that its schema feature emits application/ld+json; an older microdata implementation will not. A bespoke module is cleaner when bundles need different types or exact control over existing markup.
Keep the conversion in a service that accepts a node and returns an array of schema data. The display layer should decide where the script appears and handle encoding, while the service owns the field mapping. This separation allows a node teaser, print view, search result, and canonical page to use different visibility rules without duplicating the schema logic.
For developers who need finer control, the Drupal hook interaction guide explains how event hooks and alter hooks fit into extension work. An alter hook can attach JSON-LD to a full node build, while the service remains independent of Drupal’s rendering API. The following example deliberately skips unpublished nodes and teaser views:
function schema_jsonld_node_view_alter(
NodeInterface $node,
ViewDisplayInterface $view,
array &$build
): void {
if ($view->getMode() !== 'full' || !$node->isPublished()) {
return;
}
$data = \Drupal::service('schema_jsonld.builder')->build($node);
if (!$data) {
return;
}
$json = json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG
);
if ($json === FALSE) {
return;
}
$build['#attached']['html'][] = sprintf(
'<script type="application/ld+json">%s</script>',
$json
);
}
JSON_HEX_TAG prevents a value such as </script> from terminating the script element. The service should also return no data for node types that have no mapping, and the site should never emit a second copy through a theme or block.
Create type-specific schema templates
A reusable builder should begin with common fields: context, type, canonical URL, title, and entity identifier. It can then add properties required by the selected schema. This avoids maintaining a separate copy of Article, Event, and JobPosting data whenever the site’s main template changes.
Bundle-specific mapping belongs in exported Drupal configuration or small mapping plugins. The builder can then select a type and merge only the fields relevant to that bundle:
$type = $bundleTypes[$node->bundle()] ?? NULL;
if ($type === NULL) {
return [];
}
$schema = [
'@context' => 'https://schema.org',
'@type' => $type,
'@id' => $node->toUrl('canonical', ['absolute' => TRUE])->toString(),
'url' => $node->toUrl('canonical')->toString(),
'headline' => $node->getTitle(),
];
Relationships should use the matching schema entities. A node that references an organisation can link to an Organization; a local office can be a LocalBusiness with a PostalAddress; and a media entity can become an ImageObject. Reuse a shared @id for the same organisation or place instead of creating subtly different copies in every node.
Use arrays for genuinely repeatable properties, such as multiple authors or event performers. Keep names, URLs, dates, and identifiers consistent with what visitors see. A stable mapping makes it easier to correct a field once rather than editing several separate JSON blocks.
Represent Australian business and location details
Australian locations need more care than a country-level label. A Melbourne venue might display “Level 2, 120 Collins Street, Melbourne VIC 3000”, while Sydney and Brisbane branches use their own NSW and QLD postcodes. In PostalAddress, keep addressCountry as AU and the postcode as four characters; a Darwin address beginning with 0800 must not lose its leading zero.
Telephone values should use international notation. A Melbourne number shown locally as (03) 9876 5000 becomes +61 3 9876 5000, while a mobile number beginning 04 becomes +61 4. Store the presentation value for humans separately if needed, but put the international form in telephone. Opening hours and areaServed should likewise reflect the actual Australian branch, not a copied city-wide default.
For the Australian market, use AUD for priceCurrency, and show public prices with the dollar sign and two decimals. Dates may be displayed as 08/03/2025, but JSON-LD should use ISO 8601, with the offset derived from the site’s configured Australian timezone. Australian organisations can also publish their ABN as an identifier using PropertyValue and propertyID set to ABN.
Test every relevant node view
Run the output through the Schema Markup Validator to check JSON syntax and vocabulary, then use the Rich Results Test to see whether a supported search appearance may be available. These checks answer different questions: valid Schema.org markup can still lack the properties required by a search engine’s enhanced result.
Test the full page for every node bundle and meaningful field state. Include a normal article, one without an author or image, an event with a sold-out date, a local business without coordinates, and a node with multiple performers. Confirm that empty values are omitted and that unpublished, teaser, search, and print responses do not expose incomplete markup.
Inspect the rendered source rather than only the developer’s JSON response. There should be one JSON-LD object per described page, and its URL, dates, headline, and author should match the visible node. Search Console can reveal syntax errors and eligible items at scale; a sudden drop after a module update often points to a changed field mapping or duplicated renderer.
Deploy and maintain structured data
Treat the JSON-LD builder as part of Drupal’s render pipeline. Keep the service cacheable, but make sure its output varies by node and is invalidated when that node changes. Avoid fetching external data during rendering, because network calls can slow the response and produce unstable markup. The metadata should be derived from data Drupal already has.
Export bundle mappings and other configuration through the normal deployment process, while shipping the custom module through the established code workflow. Test compatibility with the site’s Drupal core version and contributed modules after Composer updates. A staging check should confirm that the script remains present in cached and uncached pages before production receives the release.
Document each schema type beside its content model, including required fields and editorial ownership. Re-run validation after a content-model, theme, or SEO-module change, and review Search Console reports on a regular schedule. With a versioned field map, automatic rendering, and routine validation, Drupal keeps its structured data aligned with the published content.