Custom Schema Types
When to Use
Schema Metatag ships with 25 top-level Schema.org types. When your content needs a type not in that list — LocalBusiness, MedicalEntity, LegalService, EducationalOrganization, SportsTeam — you create a custom plugin. Since Schema Metatag 2.2.0, plugins use PHP 8 attributes instead of annotation arrays, making them significantly simpler to write.
Decision
| Situation | Choice | Why |
|---|---|---|
| Needed type is in the 25 built-in types | Use existing submodule | No custom code; simpler maintenance |
| Needed type is close to a built-in (e.g., NewsArticle extends Article) | Extend the existing plugin | Inherits all parent properties |
| Completely missing type (LocalBusiness, MedicalEntity) | Create custom plugin | Full control over properties |
| Type needed in one project only | Custom plugin in custom module | Keep contrib clean |
| Type useful across many projects | Consider contrib issue/patch | Share with community |
| Schema.org Blueprints is in use | drush schemadotorg:create-type | No custom PHP needed |
Built-in Types to Extend vs Start Fresh
| Schema.org type | Built-in parent | Approach |
|---|---|---|
| NewsArticle | Article | schema_article covers it; use @type token |
| BlogPosting | Article | schema_article covers it; use @type token |
| LocalBusiness | Organization | Extend organization plugin |
| FoodEstablishment | LocalBusiness | New plugin extending LocalBusiness |
| MedicalOrganization | Organization | New plugin |
| GovernmentOrganization | Organization | New plugin |
| EducationalOrganization | Organization | New plugin |
Pattern
Plugin Structure (Attribute-based, Schema Metatag 2.2.0+)
File: modules/custom/my_module/src/Plugin/metatag/Tag/SchemaLocalBusiness.php
<?php
namespace Drupal\my_module\Plugin\metatag\Tag;
use Drupal\schema_metatag\Plugin\metatag\Tag\SchemaOrganizationBase;
use Drupal\schema_metatag\SchemaMetatagManager;
/**
* Provides a plugin for LocalBusiness schema type.
*/
#[\Drupal\schema_metatag\Attribute\SchemaType(
id: 'schema_local_business',
label: new \Drupal\Core\StringTranslation\TranslatableMarkup('Schema.org: LocalBusiness'),
description: new \Drupal\Core\StringTranslation\TranslatableMarkup('A locally-based business, including restaurants, stores, and services.'),
group: 'schema_local_business',
weight: 10,
type: 'LocalBusiness',
multiple: FALSE,
)]
class SchemaLocalBusiness extends SchemaOrganizationBase {
/**
* {@inheritdoc}
*/
public static function defaultInputValues(): array {
$items = parent::defaultInputValues();
// Add LocalBusiness-specific properties
$items['telephone'] = '';
$items['priceRange'] = '';
$items['address'] = [
'@type' => 'PostalAddress',
'streetAddress' => '',
'addressLocality' => '',
'addressRegion' => '',
'postalCode' => '',
'addressCountry' => '',
];
$items['openingHours'] = '';
$items['geo'] = [
'@type' => 'GeoCoordinates',
'latitude' => '',
'longitude' => '',
];
return $items;
}
}
Group Definition
Define the metatag group that holds this type. Create modules/custom/my_module/src/Plugin/metatag/Group/SchemaLocalBusiness.php:
<?php
namespace Drupal\my_module\Plugin\metatag\Group;
use Drupal\metatag\Plugin\metatag\Group\GroupBase;
/**
* Provides a metatag group for LocalBusiness schema.
*/
#[\Drupal\metatag\Attribute\MetatagGroup(
id: 'schema_local_business',
label: new \Drupal\Core\StringTranslation\TranslatableMarkup('Schema.org: LocalBusiness'),
description: new \Drupal\Core\StringTranslation\TranslatableMarkup('Schema.org type for local businesses.'),
weight: 20,
)]
class SchemaLocalBusiness extends GroupBase {}
Token Mapping in Admin UI
After enabling the module (drush en my_module && drush cr), configure in the Metatag admin UI at /admin/config/search/metatag. The LocalBusiness tab will appear in the Schema.org section:
@type: LocalBusiness
name: [node:title]
telephone: [node:field_phone]
priceRange: [node:field_price_range]
address > streetAddress: [node:field_address:address_line1]
address > addressLocality: [node:field_address:locality]
address > addressRegion: [node:field_address:administrative_area]
address > postalCode: [node:field_address:postal_code]
address > addressCountry: [node:field_address:country_code]
geo > latitude: [node:field_geolocation:lat]
geo > longitude: [node:field_geolocation:lng]
openingHours: Mo-Fr 09:00-17:00
url: [node:url:absolute]
Extending Without a Full Plugin
For minor additions to an existing type (e.g., adding one property to Article), use hook_metatag_tags_alter():
/**
* Implements hook_metatag_tags_alter().
*/
function my_module_metatag_tags_alter(array &$definitions): void {
// Add a custom property to the existing Article schema plugin
if (isset($definitions['schema_article'])) {
// Not the recommended approach for structural changes
// Use a full custom plugin for significant additions
}
}
Common Mistakes
- Wrong: Using annotation arrays (
@SchemaMetatag(...)) syntax from Schema Metatag < 2.2.0 → Right: Use PHP 8 attributes (#[SchemaType(...)]) for any new plugin; annotations are deprecated - Wrong: Creating a custom plugin for a type that's really just a subtype of an existing one → Right: For Article subtypes (NewsArticle, BlogPosting), just set the
@typetoken value on the built-in schema_article plugin; no custom plugin needed - Wrong: Forgetting to define the Group plugin alongside the Tag plugin → Right: Both Tag and Group plugins are required for the admin UI tab to appear correctly
- Wrong: Not clearing caches after enabling the custom module → Right: Run
drush crafter any plugin file changes; Drupal caches plugin discovery - Wrong: Hardcoding address values in the plugin class → Right: Use the defaultInputValues() method to set empty defaults; populate via tokens in the admin UI
- Wrong: Skipping the Rich Results Test after implementing a custom type → Right: Google's validator will catch property name errors and missing required fields that PHP does not