<?php
|
namespace JVBase\managers\SEO;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Schema.org Builder - Fluent API for field and type definitions
|
*
|
* Usage:
|
* - Define fields: $schema->field('custom_name')->type('text')->label('Custom Label')
|
* - Use presets: $schema->preset('name')->label('Override Label')
|
* - Define types: $schema->type('WebSite')->fields(['name', 'url', 'description'])
|
*/
|
class SchemaBuilder
|
{
|
private static ?self $instance = null;
|
private array $fieldDefinitions = [];
|
private array $typeDefinitions = [];
|
private array $typeGroups = [];
|
|
private ?FieldBuilder $currentField = null;
|
private ?TypeBuilder $currentType = null;
|
|
public array $schemaTypes = [
|
'WebSite' => 'Web Site',
|
'Organization' => 'Organization',
|
'LocalBusiness' => ' - Local Business',
|
'TattooParlor' => ' - - Tattoo Shop',
|
'HealthBusiness' => ' - - Health Business',
|
'FoodEstablishment' => ' - - Restaurant',
|
'WebPage' => 'Web Page',
|
'CollectionPage' => ' - Collection Page',
|
'DefinedTermSet' => ' - Glossary/Collection',
|
'FAQPage' => ' - FAQ Page',
|
'Person' => 'Person',
|
'CreativeWork' => 'Creative Work',
|
'DefinedTerm' => ' - Defined Term',
|
'VisualArtwork' => ' - Visual Artwork',
|
'Tattoo' => ' - - Tattoo',
|
'BeforeAfter' => ' - Before & After',
|
'Product' => 'Product',
|
'Event' => 'Event',
|
];
|
|
private array $metaFields = ['metaTitle', 'metaDescription', 'socialPreviewImage', 'twitterImage'];
|
|
private array $defaultMetaValues = [
|
'metaTitle' => '{{post_title}}',
|
'metaDescription' => '{{post_excerpt}}',
|
'socialPreviewImage' => '{{featured_image}}',
|
'twitterImage' => ''
|
];
|
|
public static function getInstance(): self
|
{
|
if (self::$instance === null) {
|
self::$instance = new self();
|
}
|
return self::$instance;
|
}
|
|
private function __construct()
|
{
|
$this->registerPresetFields();
|
$this->registerTypes();
|
$this->registerTypeGroups();
|
|
do_action(BASE . 'schema_builder_loaded', $this);
|
}
|
|
/**
|
* Start defining a custom field
|
*/
|
public function field(string $name): FieldBuilder
|
{
|
$this->currentField = new FieldBuilder($this, $name);
|
return $this->currentField;
|
}
|
|
/**
|
* Start with a preset field (can be customized)
|
*/
|
public function preset(string $name): FieldBuilder
|
{
|
$presets = $this->getPresetDefinitions();
|
|
if (!isset($presets[$name])) {
|
throw new \InvalidArgumentException("Unknown preset field: {$name}");
|
}
|
|
$this->currentField = new FieldBuilder($this, $name, $presets[$name]);
|
return $this->currentField;
|
}
|
|
/**
|
* Start defining a schema type
|
*/
|
public function type(string $typeName): TypeBuilder
|
{
|
$this->currentType = new TypeBuilder($this, $typeName);
|
return $this->currentType;
|
}
|
|
/**
|
* Register a custom field definition
|
*/
|
public function registerField(string $fieldName, array $config): void
|
{
|
$this->fieldDefinitions[$fieldName] = $config;
|
}
|
|
/**
|
* Register a custom type definition
|
*/
|
public function registerType(string $typeName, array $config): void
|
{
|
$this->typeDefinitions[$typeName] = $config;
|
}
|
|
/**
|
* Get field definition
|
*/
|
public function getFieldDefinition(string $fieldName): ?array
|
{
|
$definitions = apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
|
return $definitions[$fieldName] ?? null;
|
}
|
|
|
/**
|
* Get type definition
|
*/
|
public function getTypeDefinition(string $type): ?array
|
{
|
$definitions = $this->getTypeDefinitions();
|
return $definitions[$type] ?? null;
|
}
|
|
/**
|
* Get all type definitions
|
*/
|
public function getTypeDefinitions(): array
|
{
|
return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions);
|
}
|
|
public function getTypeGroups(): array
|
{
|
return $this->typeGroups;
|
}
|
|
public function getMetaFields(): array
|
{
|
return $this->metaFields;
|
}
|
|
public function getDefaultMetaValues(): array
|
{
|
return $this->defaultMetaValues;
|
}
|
|
/**
|
* Get all fields for a specific type (with inheritance)
|
*/
|
public function getFieldsForType(string $type): array
|
{
|
$fields = [];
|
|
$typeDefinition = $this->getTypeDefinition($type);
|
if (!$typeDefinition) {
|
return $fields;
|
}
|
|
$fields = array_merge($fields, $typeDefinition['fields'] ?? []);
|
|
// Handle inheritance
|
if (!empty($typeDefinition['extends'])) {
|
$parentFields = $this->getFieldsForType($typeDefinition['extends']);
|
$fields = array_unique(array_merge($parentFields, $fields));
|
}
|
|
return $fields;
|
}
|
|
/**
|
* Get Meta configuration for a schema type
|
* This creates the form fields for the selected @type
|
*/
|
public function getMetaConfigForType(string $type): array
|
{
|
$fields = $this->getFieldsForType($type);
|
$config = [];
|
|
foreach ($fields as $fieldName) {
|
$fieldDef = $this->getFieldDefinition($fieldName);
|
if ($fieldDef) {
|
// Use the field name as the key (this IS the schema property)
|
$config[$fieldName] = $fieldDef;
|
}
|
}
|
|
return $config;
|
}
|
|
/**
|
* Get types organized by group for UI display
|
*/
|
public function getTypesByGroup(): array
|
{
|
$types = $this->getTypeDefinitions();
|
$grouped = [];
|
|
foreach ($types as $typeName => $config) {
|
$group = $config['group'] ?? 'general';
|
|
if (!isset($grouped[$group])) {
|
$grouped[$group] = [
|
'label' => $this->typeGroups[$group] ?? ucfirst($group),
|
'types' => []
|
];
|
}
|
|
$grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName;
|
}
|
|
return $grouped;
|
}
|
|
/**
|
* Register a type group
|
*/
|
public function registerGroup(string $key, string $label): void
|
{
|
$this->typeGroups[$key] = $label;
|
}
|
|
/**
|
* Get post types for select options
|
*/
|
public static function getContentPostTypes(): array
|
{
|
$options = ['' => '-- Select Post Type --'];
|
|
if (defined('JVB_CONTENT')) {
|
foreach (JVB_CONTENT as $key => $config) {
|
$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
|
}
|
}
|
|
return $options;
|
}
|
|
/**
|
* Get taxonomies for select options
|
*/
|
public static function getContentTaxonomies(): array
|
{
|
$options = ['' => '-- Select Taxonomy --'];
|
|
if (defined('JVB_TAXONOMY')) {
|
foreach (JVB_TAXONOMY as $key => $config) {
|
$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
|
}
|
}
|
|
return $options;
|
}
|
|
/**
|
* Define preset fields that can be reused
|
*/
|
private function registerPresetFields(): void
|
{
|
// Special type selector field
|
$this->field('type')
|
->type('select')
|
->label('Type')
|
->options(array_merge(['' => '-- Content Type'], $this->schemaTypes));
|
|
/**************************************************************
|
* META FIELDS
|
**************************************************************/
|
$this->field('metaTitle')
|
->type('text')
|
->label('Meta Title')
|
->description('Used in search results and when shared on social media. Leave blank to use default.');
|
|
$this->field('metaDescription')
|
->type('textarea')
|
->label('Meta Description')
|
->description('Brief description shown in search results and social previews.');
|
|
$this->field('socialPreviewImage')
|
->type('group')
|
->label('Social Preview Image')
|
->description('Image shown when shared on social media. Recommended: 1200x630px.')
|
->transformer('image_url_with_fallback')
|
->fields([
|
'source_field' => [
|
'type' => 'text',
|
'label' => 'Image Source Field',
|
'description' => 'Template field to get image from (e.g., {{post_thumbnail}}, {{custom_image_field}})',
|
'placeholder' => '{{post_thumbnail}}'
|
],
|
'fallback' => [
|
'type' => 'upload',
|
'label' => 'Fallback Image',
|
'description' => 'Used when source field returns no image'
|
]
|
]);
|
|
$this->field('twitterImage')
|
->type('group')
|
->label('Twitter Card Image')
|
->description('Separate image for Twitter. Falls back to main social image if empty.')
|
->transformer('image_url_with_fallback')
|
->fields([
|
'source_field' => [
|
'type' => 'text',
|
'label' => 'Image Source Field',
|
'placeholder' => '{{twitter_specific_image}}'
|
],
|
'fallback' => [
|
'type' => 'upload',
|
'label' => 'Fallback Image'
|
]
|
]);
|
|
/**************************************************************
|
* QA FIELD FAQ
|
**************************************************************/
|
$this->field('question')
|
->type('text')
|
->label('Question')
|
->description('Template for the question (e.g., {{post_title}})')
|
->default('{{post_title}}')
|
->transformer('text');
|
|
$this->field('answer')
|
->type('textarea')
|
->label('Answer')
|
->description('Template for the answer (e.g., {{post_content}})')
|
->default('{{post_content}}')
|
->transformer('text');
|
/**************************************************************
|
* CORE IDENTITY FIELDS
|
**************************************************************/
|
$this->field('name')
|
->type('text')
|
->label('Name')
|
->description('The name of the item')
|
->transformer('text');
|
|
$this->field('alternateName')
|
->type('repeater')
|
->label('Alternate Name(s)')
|
->description('Alternative names or nicknames')
|
->transformer('text_array')
|
->fields([
|
'name' => [
|
'type' => 'text',
|
'label' => 'Name'
|
]
|
]);
|
|
$this->field('legalName')
|
->type('text')
|
->label('Legal Name')
|
->description('The official legal name')
|
->transformer('text');
|
|
$this->field('description')
|
->type('textarea')
|
->label('Description')
|
->description('A description of the item')
|
->transformer('text');
|
|
$this->field('disambiguatingDescription')
|
->type('textarea')
|
->label('Disambiguating Description')
|
->description('Brief clarification to distinguish from similar items')
|
->transformer('text');
|
|
$this->field('url')
|
->type('url')
|
->label('URL')
|
->description('Website URL')
|
->transformer('url');
|
|
$this->field('slogan')
|
->type('text')
|
->label('Slogan')
|
->description('A slogan or tagline')
|
->transformer('text');
|
|
/**************************************************************
|
* BEFORE/AFTER FIELDS
|
**************************************************************/
|
$this->field('about')
|
->type('reference')
|
->label('About')
|
->transformer('reference');
|
|
$this->field('temporalCoverage')
|
->type('text')
|
->label('Time Period')
|
->description('ISO 8601 format: 2024-01-10/2024-09-01')
|
->transformer('text');
|
|
$this->field('associatedMedia')
|
->type('repeater')
|
->label('Associated Media')
|
->transformer('image_object_array')
|
->fields([
|
'image' => ['type' => 'image', 'label' => 'Image'],
|
'caption' => ['type' => 'text', 'label' => 'Caption'],
|
'position' => ['type' => 'number', 'label' => 'Position'],
|
]);
|
|
$this->field('additionalProperty')
|
->type('repeater')
|
->label('Additional Properties')
|
->transformer('property_value_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Property Name'],
|
'value' => ['type' => 'text', 'label' => 'Value'],
|
]);
|
|
/**************************************************************
|
* IMAGE FIELDS
|
**************************************************************/
|
$this->field('image')
|
->type('upload')
|
->label('Image')
|
->description('Primary image')
|
->transformer('image_object');
|
|
$this->field('logo')
|
->type('upload')
|
->label('Logo')
|
->transformer('image_object');
|
|
$this->field('photo')
|
->type('upload')
|
->label('Photo of Location')
|
->transformer('image_object');
|
|
$this->field('video')
|
->type('upload')
|
->label('Video')
|
->transformer('video_object');
|
|
/**************************************************************
|
* LOCATION & CONTACT FIELDS
|
**************************************************************/
|
$this->field('location')
|
->type('location')
|
->label('Location')
|
->description('Physical location with address and coordinates')
|
->transformer('location_complex');
|
|
$this->field('address')
|
->type('location')
|
->label('Address')
|
->description('Postal address')
|
->transformer('postal_address');
|
|
$this->field('geo')
|
->type('group')
|
->label('Geographic Coordinates')
|
->description('Latitude and longitude')
|
->transformer('geo_coordinates')
|
->fields([
|
'latitude' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Latitude',
|
],
|
'longitude' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Longitude',
|
]
|
]);
|
|
$this->field('telephone')
|
->type('text')
|
->label('Telephone')
|
->description('Phone number')
|
->transformer('text');
|
|
$this->field('faxNumber')
|
->type('text')
|
->label('Fax Number')
|
->transformer('text');
|
|
$this->field('email')
|
->type('email')
|
->label('Email')
|
->description('Email address')
|
->transformer('email');
|
|
$this->field('contactPoint')
|
->type('repeater')
|
->label('Contact Points')
|
->description('Additional contact methods')
|
->transformer('contact_point_array')
|
->fields([
|
'contactType' => [
|
'type' => 'text',
|
'label' => 'Contact Type',
|
'description' => 'e.g., customer service, sales',
|
],
|
'telephone' => [
|
'type' => 'text',
|
'label' => 'Phone',
|
],
|
'email' => [
|
'type' => 'email',
|
'label' => 'Email',
|
]
|
]);
|
|
$this->field('potentialAction')
|
->type('repeater')
|
->label('Potential Actions')
|
->transformer('potential_action_array')
|
->fields([
|
'action' => [
|
'type' => 'radio',
|
'label' => 'Action',
|
'options' => [
|
'searchAction' => 'Search Action',
|
'communicateAction' => 'Contact Action',
|
'scheduleAction' => 'Reserve Action',
|
'applyAction' => 'Estimate Action'
|
]
|
],
|
'name' => [
|
'type' => 'text',
|
'label' => 'Name',
|
],
|
'target' => [
|
'type' => 'url',
|
'label' => 'Action URL',
|
],
|
'description' => [
|
'type' => 'textarea',
|
'label' => 'Description'
|
]
|
])
|
->default([
|
[
|
'action' => 'searchAction',
|
'target' => get_home_url(null, '/search/?s={query}')
|
]
|
]);
|
|
/**************************************************************
|
* HOURS & OPERATIONAL FIELDS
|
**************************************************************/
|
$this->field('openingHours')
|
->type('group')
|
->label('Opening Hours')
|
->description('Business hours specification')
|
->transformer('opening_hours_specification')
|
->fields([
|
'monday' => [
|
'type' => 'group',
|
'label' => 'Monday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'tuesday' => [
|
'type' => 'group',
|
'label' => 'Tuesday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'wednesday' => [
|
'type' => 'group',
|
'label' => 'Wednesday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'thursday' => [
|
'type' => 'group',
|
'label' => 'Thursday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'friday' => [
|
'type' => 'group',
|
'label' => 'Friday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'saturday' => [
|
'type' => 'group',
|
'label' => 'Saturday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
'sunday' => [
|
'type' => 'group',
|
'label' => 'Sunday',
|
'fields' => [
|
'opens' => ['type' => 'time', 'label' => 'Opens'],
|
'closes' => ['type' => 'time', 'label' => 'Closes']
|
]
|
],
|
]);
|
|
$this->field('hasPart')
|
->type('repeater')
|
->label('Site Navigation')
|
->description('Main navigation menu items')
|
->transformer('navigation_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Link Text'],
|
'url' => ['type' => 'url', 'label' => 'URL'],
|
'description' => ['type' => 'textarea', 'label' => 'Description (optional)'],
|
]);
|
|
$this->field('priceRange')
|
->type('text')
|
->label('Price Range')
|
->description('e.g., $$, $100-$500')
|
->transformer('text');
|
|
$this->field('currenciesAccepted')
|
->type('checkbox')
|
->label('Currencies Accepted')
|
->options(['CAD' => 'CAD', 'USD' => 'USD'])
|
->transformer('text_array');
|
|
$this->field('paymentAccepted')
|
->type('checkbox')
|
->label('Payment Methods')
|
->options([
|
'Cash' => 'Cash',
|
'Credit Card' => 'Credit Card',
|
'Debit' => 'Debit',
|
'Google Pay' => 'Google Pay',
|
'Apple Pay' => 'Apple Pay',
|
'PayPal' => 'PayPal',
|
'Interac' => 'Interac',
|
'AMEX' => 'AMEX',
|
])
|
->transformer('text_array');
|
|
/**************************************************************
|
* ORGANIZATION & BUSINESS FIELDS
|
**************************************************************/
|
$this->field('foundingDate')
|
->type('date')
|
->label('Founding Date')
|
->description('Date the organization was founded')
|
->transformer('date');
|
|
$this->field('dissolutionDate')
|
->type('date')
|
->label('Dissolution Date')
|
->description('Date the organization closed')
|
->transformer('date');
|
|
$this->field('founders')
|
->type('repeater')
|
->label('Founders')
|
->description('Name of founder(s)')
|
->transformer('person_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Name'],
|
'url' => ['type' => 'url', 'label' => 'URL'],
|
]);
|
|
$this->field('numberOfEmployees')
|
->type('text')
|
->label('Number of Employees')
|
->transformer('number');
|
|
$this->field('taxID')
|
->type('text')
|
->label('Tax ID')
|
->description('Tax identification number')
|
->transformer('text');
|
|
$this->field('vatID')
|
->type('text')
|
->label('VAT ID')
|
->description('VAT registration number')
|
->transformer('text');
|
|
$this->field('duns')
|
->type('text')
|
->label('D-U-N-S Number')
|
->description('Dun & Bradstreet number')
|
->transformer('text');
|
|
/**************************************************************
|
* SOCIAL & LINKS
|
**************************************************************/
|
$this->field('sameAs')
|
->type('repeater')
|
->label('Social Media & Links')
|
->description('URLs to social profiles and related pages')
|
->transformer('url_array')
|
->fields([
|
'url' => ['type' => 'url', 'label' => 'URL']
|
]);
|
|
/**************************************************************
|
* AREA & GEOGRAPHY
|
**************************************************************/
|
$this->field('areaServed')
|
->type('repeater')
|
->label('Area Served')
|
->description('Geographic areas served')
|
->transformer('text_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Location Name'],
|
'url' => ['type' => 'url', 'label' => 'Wikipedia Page'],
|
]);
|
|
$this->field('hasMap')
|
->type('url')
|
->label('Map URL')
|
->description('Link to a map (e.g., Google Maps)')
|
->transformer('url');
|
|
/**************************************************************
|
* AMENITIES & FEATURES
|
**************************************************************/
|
$this->field('amenityFeature')
|
->type('checkbox')
|
->label('Amenity Features')
|
->description('Available facilities and features')
|
->transformer('text')
|
->options([
|
'Wheelchair Accessible' => 'Wheelchair Accessible',
|
'Free Parking' => 'Free Parking',
|
'Private Rooms' => 'Private Rooms',
|
'Air Conditioning' => 'Air Conditioning',
|
'WiFi' => 'WiFi',
|
'Gender Neutral Restroom' => 'Gender Neutral Restroom',
|
'LGBTQ+ Friendly' => 'LGBTQ+ Friendly',
|
'Sterilization Room' => 'Sterilization Room',
|
'Refreshments Available' => 'Refreshments Available',
|
'Street Level Access' => 'Street Level Access',
|
'Single Use Needles' => 'Single Use Needles',
|
'Consultation Room' => 'Consultation Room',
|
'Aftercare Products Available' => 'Aftercare Products Available',
|
'Walk-Ins Welcome' => 'Walk-Ins Welcome',
|
'By Appointment' => 'By Appointment Only',
|
]);
|
|
/**************************************************************
|
* LANGUAGES
|
**************************************************************/
|
$this->field('availableLanguage')
|
->type('repeater')
|
->label('Languages Available')
|
->description('Languages spoken or supported')
|
->transformer('language_array')
|
->fields([
|
'language' => ['type' => 'text', 'label' => 'Language']
|
]);
|
|
$this->field('knowsLanguage')
|
->type('repeater')
|
->label('Languages Known')
|
->description('Languages the person knows')
|
->transformer('language_array')
|
->fields([
|
'language' => ['type' => 'text', 'label' => 'Language']
|
]);
|
|
$this->field('inLanguage')
|
->type('radio')
|
->label('In Language')
|
->options([
|
'en-CA' => 'English, Canadian',
|
'en-US' => 'English, American',
|
'fr-CA' => 'French, Canadian'
|
])
|
->transformer('text');
|
|
/**************************************************************
|
* RATINGS & REVIEWS
|
**************************************************************/
|
$this->field('aggregateRating')
|
->type('group')
|
->label('Aggregate Rating')
|
->description('Overall rating and review count')
|
->transformer('aggregate_rating')
|
->fields([
|
'ratingValue' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Rating Value',
|
'description' => 'Average rating (e.g., 4.5)',
|
],
|
'bestRating' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Best Rating',
|
'default' => 5,
|
'description' => 'Highest possible rating (e.g., 5)',
|
],
|
'worstRating' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Worst Rating',
|
'default' => 1,
|
'description' => 'Lowest possible rating (e.g., 1)',
|
],
|
'ratingCount' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Rating Count',
|
'description' => 'Total number of ratings',
|
],
|
'reviewCount' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Review Count',
|
'description' => 'Total number of reviews',
|
]
|
]);
|
|
/**************************************************************
|
* KEYWORDS & CATEGORIZATION
|
**************************************************************/
|
$this->field('keywords')
|
->type('repeater')
|
->label('Keywords')
|
->description('Keywords or tags')
|
->transformer('text_array')
|
->fields([
|
'keyword' => ['type' => 'text', 'label' => 'Keyword']
|
]);
|
|
/**************************************************************
|
* PERSON FIELDS
|
**************************************************************/
|
$this->field('givenName')
|
->type('text')
|
->label('First Name')
|
->transformer('text');
|
|
$this->field('familyName')
|
->type('text')
|
->label('Last Name')
|
->transformer('text');
|
|
$this->field('honorificPrefix')
|
->type('text')
|
->label('Honorific Prefix')
|
->description('e.g., Dr., Mr., Ms.')
|
->transformer('text');
|
|
$this->field('honorificSuffix')
|
->type('text')
|
->label('Honorific Suffix')
|
->description('e.g., PhD, MD')
|
->transformer('text');
|
|
$this->field('jobTitle')
|
->type('text')
|
->label('Job Title')
|
->transformer('text');
|
|
$this->field('birthDate')
|
->type('date')
|
->label('Birth Date')
|
->description('For public figures')
|
->transformer('date');
|
|
$this->field('gender')
|
->type('text')
|
->label('Gender')
|
->transformer('text');
|
|
/**************************************************************
|
* CREATIVE WORK FIELDS
|
**************************************************************/
|
$this->field('author')
|
->type('text')
|
->label('Author')
|
->description('Author name or reference')
|
->transformer('person_reference');
|
|
$this->field('creator')
|
->type('text')
|
->label('Creator')
|
->description('Creator name or reference')
|
->transformer('text');
|
|
$this->field('dateCreated')
|
->type('text')
|
->label('Date Created')
|
->transformer('text');
|
|
$this->field('datePublished')
|
->type('text')
|
->label('Date Published')
|
->default('{{post_date')
|
->transformer('text');
|
|
$this->field('dateModified')
|
->type('text')
|
->default('{{post_modified}}')
|
->label('Date Modified')
|
->transformer('text');
|
|
/**************************************************************
|
* VISUAL ARTWORK FIELDS
|
**************************************************************/
|
$this->field('artform')
|
->type('text')
|
->label('Art Form')
|
->description('e.g., Painting, Sculpture, Tattoo')
|
->transformer('text');
|
|
$this->field('artMedium')
|
->type('text')
|
->label('Art Medium')
|
->description('e.g., Oil, Watercolor, Ink')
|
->transformer('text');
|
|
$this->field('artworkSurface')
|
->type('text')
|
->label('Artwork Surface')
|
->description('e.g., Canvas, Paper, Skin')
|
->transformer('text');
|
|
$this->field('width')
|
->type('text')
|
->label('Width')
|
->description('Width with unit (e.g., 10cm, 5in)')
|
->transformer('dimension');
|
|
$this->field('height')
|
->type('text')
|
->label('Height')
|
->description('Height with unit (e.g., 15cm, 8in)')
|
->transformer('dimension');
|
|
/**************************************************************
|
* EVENT FIELDS
|
**************************************************************/
|
$this->field('startDate')
|
->type('text')
|
->default('{{start_date}}')
|
->label('Start Date/Time')
|
->transformer('text');
|
|
$this->field('endDate')
|
->type('text')
|
->default('{{end_date}}')
|
->label('End Date/Time')
|
->transformer('text');
|
|
$this->field('eventStatus')
|
->type('select')
|
->label('Event Status')
|
->options([
|
'https://schema.org/EventScheduled' => 'Scheduled',
|
'https://schema.org/EventCancelled' => 'Cancelled',
|
'https://schema.org/EventPostponed' => 'Postponed',
|
'https://schema.org/EventRescheduled' => 'Rescheduled',
|
])
|
->transformer('text');
|
|
$this->field('eventAttendanceMode')
|
->type('select')
|
->label('Attendance Mode')
|
->options([
|
'https://schema.org/OfflineEventAttendanceMode' => 'In-Person',
|
'https://schema.org/OnlineEventAttendanceMode' => 'Online',
|
'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid',
|
])
|
->transformer('text');
|
|
/**************************************************************
|
* PRODUCT FIELDS
|
**************************************************************/
|
$this->field('brand')
|
->type('group')
|
->label('Brand')
|
->transformer('brand_object')
|
->fields([
|
'type' => [
|
'type' => 'select',
|
'label' => 'Brand Type',
|
'options' => [
|
'text' => 'Text Only',
|
'organization' => 'Organization/Brand',
|
]
|
],
|
'name' => [
|
'type' => 'text',
|
'label' => 'Brand Name',
|
],
|
'url' => [
|
'type' => 'url',
|
'label' => 'Brand Website',
|
'condition' => [
|
'field' => 'type',
|
'value' => 'organization'
|
]
|
],
|
'logo' => [
|
'type' => 'upload',
|
'label' => 'Brand Logo',
|
'condition' => [
|
'field' => 'type',
|
'value' => 'organization'
|
]
|
],
|
]);
|
|
$this->field('sku')
|
->type('text')
|
->label('SKU')
|
->description('Stock Keeping Unit')
|
->transformer('text');
|
|
$this->field('gtin')
|
->type('text')
|
->label('GTIN')
|
->description('Global Trade Item Number')
|
->transformer('text');
|
|
/**************************************************************
|
* SERVICES & OFFERS
|
**************************************************************/
|
$this->field('hasOfferCatalog')
|
->type('group')
|
->label('Offer Catalog')
|
->transformer('offer_catalog_array')
|
->fields([
|
'source' => [
|
'type' => 'select',
|
'label' => 'Source',
|
'options' => [
|
'auto' => 'Auto from post type',
|
'manual' => 'Manual entry',
|
]
|
],
|
'post_type' => [
|
'type' => 'select',
|
'label' => 'Post Type',
|
'options' => self::getContentPostTypes(),
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'group_by_taxonomy' => [
|
'type' => 'true_false',
|
'label' => 'Group by category/taxonomy',
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'taxonomy' => [
|
'type' => 'select',
|
'label' => 'Taxonomy',
|
'options' => self::getContentTaxonomies(),
|
'condition' => ['field' => 'group_by_taxonomy', 'value' => '1']
|
],
|
'manual_items' => [
|
'type' => 'repeater',
|
'label' => 'Manual Offers',
|
'condition' => ['field' => 'source', 'value' => 'manual'],
|
'fields' => [
|
'type' => ['type' => 'radio', 'label' => 'Type', 'options' => ['Service' => 'Service', 'Product' => 'Product']],
|
'name' => ['type' => 'text', 'label' => 'Offer Name'],
|
'description' => ['type' => 'textarea', 'label' => 'Description'],
|
'price' => ['type' => 'text', 'label' => 'Price'],
|
]
|
]
|
]);
|
|
$this->field('knowsAbout')
|
->type('repeater')
|
->label('Areas of Expertise')
|
->description('Skills and specialties')
|
->transformer('text_array')
|
->fields([
|
'topic' => ['type' => 'text', 'label' => 'Topic']
|
]);
|
|
/**************************************************************
|
* CREDENTIALS & CERTIFICATIONS
|
**************************************************************/
|
$this->field('hasCredential')
|
->type('repeater')
|
->label('Credentials / Certifications')
|
->description('Professional certifications')
|
->transformer('credential_array')
|
->fields([
|
'credentialCategory' => ['type' => 'text', 'label' => 'Category'],
|
'name' => ['type' => 'text', 'label' => 'Name'],
|
'issuedBy' => ['type' => 'text', 'label' => 'Issued By']
|
]);
|
|
$this->field('award')
|
->type('repeater')
|
->label('Awards & Recognition')
|
->transformer('text_array')
|
->fields([
|
'award' => ['type' => 'text', 'label' => 'Award']
|
]);
|
|
$this->field('serviceArea')
|
->type('repeater')
|
->label('Service Areas')
|
->description('Geographic areas served (cities, neighborhoods, or radius)')
|
->transformer('service_area_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Area Name'],
|
'type' => [
|
'type' => 'select',
|
'label' => 'Type',
|
'options' => [
|
'City' => 'City',
|
'AdministrativeArea' => 'Region/Province',
|
'GeoCircle' => 'Radius',
|
]
|
],
|
'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'],
|
]);
|
|
$this->field('makesOffer')
|
->type('group')
|
->label('Featured Offerings')
|
->transformer('offers_from_posts')
|
->fields([
|
'source' => [
|
'type' => 'select',
|
'label' => 'Source',
|
'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
|
],
|
'post_type' => [
|
'type' => 'select',
|
'label' => 'Post Type',
|
'options' => self::getContentPostTypes(),
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'limit' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Featured Count',
|
'default' => 5,
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'manual_items' => [
|
'type' => 'repeater',
|
'label' => 'Manual Offers',
|
'condition' => ['field' => 'source', 'value' => 'manual'],
|
'fields' => [
|
'name' => ['type' => 'text', 'label' => 'Offer Name'],
|
'description' => ['type' => 'textarea', 'label' => 'Description'],
|
'price' => ['type' => 'text', 'label' => 'Price/Range'],
|
]
|
]
|
]);
|
|
$this->field('hasMenu')
|
->type('group')
|
->label('Menu Items')
|
->description('Auto-populate from post type or enter manually')
|
->transformer('menu_from_posts')
|
->fields([
|
'source' => [
|
'type' => 'select',
|
'label' => 'Source',
|
'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
|
],
|
'post_type' => [
|
'type' => 'select',
|
'label' => 'Post Type',
|
'options' => self::getContentPostTypes(),
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'limit' => [
|
'type' => 'text',
|
'subtype' => 'number',
|
'label' => 'Number of items',
|
'default' => 10,
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'orderby' => [
|
'type' => 'select',
|
'label' => 'Order By',
|
'options' => ['menu_order' => 'Menu Order', 'title' => 'Title', 'date' => 'Date'],
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'manual_items' => [
|
'type' => 'repeater',
|
'label' => 'Manual Items',
|
'condition' => ['field' => 'source', 'value' => 'manual'],
|
'fields' => [
|
'name' => ['type' => 'text', 'label' => 'Item Name'],
|
'description' => ['type' => 'textarea', 'label' => 'Description'],
|
'price' => ['type' => 'text', 'label' => 'Price'],
|
]
|
]
|
]);
|
|
/**************************************************************
|
* FAQ FIELDS
|
**************************************************************/
|
$this->field('faq')
|
->type('repeater')
|
->label('FAQ Items')
|
->description('Question and Answer pairs')
|
->transformer('faq_array')
|
->fields([
|
'question' => ['type' => 'text', 'label' => 'Question'],
|
'answer' => ['type' => 'text', 'label' => 'Answer']
|
]);
|
|
/**************************************************************
|
* FOOD & CUISINE
|
**************************************************************/
|
$this->field('servesCuisine')
|
->type('repeater')
|
->label('Cuisine Types')
|
->description('Types of cuisine served')
|
->transformer('text_array')
|
->fields([
|
'cuisine' => ['type' => 'text', 'label' => 'Cuisine Type', 'description' => 'e.g., Italian, Mexican, Vegan']
|
]);
|
|
$this->field('menu')
|
->type('url')
|
->label('Menu URL')
|
->description('Link to online menu')
|
->transformer('url');
|
|
/**************************************************************
|
* PRODUCT/OFFER FIELDS
|
**************************************************************/
|
$this->field('offers')
|
->type('group')
|
->label('Offer Details')
|
->description('Price and availability information')
|
->transformer('offer_object')
|
->fields([
|
'price' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Price'],
|
'priceCurrency' => ['type' => 'text', 'label' => 'Currency', 'default' => 'USD'],
|
'availability' => [
|
'type' => 'select',
|
'label' => 'Availability',
|
'options' => [
|
'InStock' => 'In Stock',
|
'PreOrder' => 'Pre-Order',
|
'SoldOut' => 'Sold Out',
|
'OutOfStock' => 'Out of Stock',
|
'Discontinued' => 'Discontinued',
|
]
|
],
|
'validFrom' => ['type' => 'text', 'label' => 'Valid From', 'default' => '{{validFrom}}'],
|
'validThrough' => ['type' => 'text', 'label' => 'Valid Through', 'default' => '{{validTo}}'],
|
]);
|
|
$this->field('mpn')
|
->type('text')
|
->label('Manufacturer Part Number')
|
->transformer('text');
|
|
/**************************************************************
|
* BUSINESS POLICIES & FEATURES
|
**************************************************************/
|
$this->field('isAccessibleForFree')
|
->type('true_false')
|
->label('Accessible For Free')
|
->description('Is this service/location accessible without payment?')
|
->transformer('boolean');
|
|
$this->field('smokingAllowed')
|
->type('true_false')
|
->label('Smoking Allowed')
|
->transformer('boolean');
|
|
$this->field('petsAllowed')
|
->type('select')
|
->label('Pets Allowed')
|
->options([
|
'' => 'Not specified',
|
'yes' => 'Yes',
|
'no' => 'No',
|
])
|
->transformer('boolean');
|
|
/**************************************************************
|
* ORGANIZATION RELATIONSHIPS
|
**************************************************************/
|
$this->field('parentOrganization')
|
->type('group')
|
->label('Parent Organization')
|
->description('Organization this is a part of')
|
->transformer('organization_reference')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Organization Name'],
|
'url' => ['type' => 'url', 'label' => 'Website'],
|
]);
|
|
$this->field('subOrganization')
|
->type('repeater')
|
->label('Sub-Organizations')
|
->description('Child organizations or departments')
|
->transformer('organization_reference_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Organization Name'],
|
'url' => ['type' => 'url', 'label' => 'Website'],
|
]);
|
|
$this->field('employee')
|
->type('repeater')
|
->label('Employees')
|
->transformer('person_reference_array')
|
->fields([
|
'name' => ['type' => 'text', 'label' => 'Name'],
|
'jobTitle' => ['type' => 'text', 'label' => 'Job Title'],
|
]);
|
|
/**************************************************************
|
* HOSPITALITY
|
**************************************************************/
|
$this->field('checkinTime')
|
->type('time')
|
->label('Check-in Time')
|
->transformer('time');
|
|
$this->field('checkoutTime')
|
->type('time')
|
->label('Check-out Time')
|
->transformer('time');
|
|
$this->field('starRating')
|
->type('group')
|
->label('Star Rating')
|
->transformer('rating_object')
|
->fields([
|
'ratingValue' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5]
|
]);
|
|
/**************************************************************
|
* REVIEW & RATING
|
**************************************************************/
|
$this->field('review')
|
->type('repeater')
|
->label('Reviews')
|
->transformer('review_array')
|
->fields([
|
'author' => ['type' => 'text', 'label' => 'Reviewer Name'],
|
'reviewRating' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5],
|
'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'],
|
'datePublished' => ['type' => 'date', 'label' => 'Date'],
|
]);
|
|
/**************************************************************
|
* HEALTH & MEDICAL
|
**************************************************************/
|
$this->field('medicalSpecialty')
|
->type('repeater')
|
->label('Medical Specialties')
|
->transformer('text_array')
|
->fields([
|
'specialty' => ['type' => 'text', 'label' => 'Specialty']
|
]);
|
|
$this->field('healthcareService')
|
->type('repeater')
|
->label('Healthcare Services')
|
->transformer('text_array')
|
->fields([
|
'service' => ['type' => 'text', 'label' => 'Service']
|
]);
|
|
/***************************************************************
|
|
***************************************************************/
|
$this->field('termCode')
|
->type('text')
|
->label('Term Code')
|
->description('Unique identifier or code for this term')
|
->transformer('text');
|
|
$this->field('hasDefinedTerm')
|
->type('group')
|
->label('Defined Terms')
|
->description('Terms included in this glossary or collection')
|
->transformer('defined_terms_from_posts')
|
->fields([
|
'source' => [
|
'type' => 'select',
|
'label' => 'Source',
|
'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
|
],
|
'post_type' => [
|
'type' => 'select',
|
'label' => 'Post Type',
|
'options' => self::getContentPostTypes(),
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
],
|
'taxonomy' => [
|
'type' => 'select',
|
'label' => 'Filter by Taxonomy',
|
'options' => self::getContentTaxonomies(),
|
'condition' => ['field' => 'source', 'value' => 'auto']
|
]
|
]);
|
}
|
|
|
/**
|
* Get raw preset definitions (before filters)
|
*/
|
private function getPresetDefinitions(): array
|
{
|
return $this->fieldDefinitions;
|
}
|
|
/**
|
* Define schema types
|
*/
|
private function registerTypes(): void
|
{
|
/**************************************************************
|
* GENERAL / SITE-WIDE
|
**************************************************************/
|
$this->type('WebSite')
|
->label('Website')
|
->group('general')
|
->fields([
|
'name',
|
'description',
|
'url',
|
'inLanguage',
|
'potentialAction',
|
'hasPart',
|
'creator',
|
]);
|
|
/**************************************************************
|
* PAGE TYPES
|
**************************************************************/
|
$this->type('WebPage')
|
->label('Web Page')
|
->group('page')
|
->fields([
|
'type',
|
'name',
|
'description',
|
'url',
|
'image',
|
'datePublished',
|
'dateModified',
|
'author',
|
]);
|
|
$this->type('CollectionPage')
|
->label('Collection Page')
|
->group('page')
|
->extends('WebPage');
|
|
$this->type('FAQPage')
|
->label('FAQ Page')
|
->group('page')
|
->extends('WebPage')
|
->addFields([
|
'question',
|
'answer'
|
]);
|
|
$this->type('Place')
|
->label('Place')
|
->group('general')
|
->fields([
|
'type',
|
'name',
|
'description',
|
'url',
|
'image',
|
'geo',
|
'address',
|
'sameAs',
|
]);
|
|
$this->type('City')
|
->label('City')
|
->group('general')
|
->extends('Place')
|
->addFields([
|
'containedInPlace',
|
]);
|
$this->field('containedInPlace')
|
->type('reference')
|
->label('Contained In')
|
->description('Parent place (province, country)')
|
->transformer('reference');
|
/**************************************************************
|
* ORGANIZATION & BUSINESS
|
**************************************************************/
|
$this->type('Organization')
|
->label('Organization')
|
->group('business')
|
->fields([
|
'type',
|
'name',
|
'legalName',
|
'alternateName',
|
'description',
|
'url',
|
'logo',
|
'image',
|
'email',
|
'telephone',
|
'sameAs',
|
'founders',
|
'foundingDate',
|
'numberOfEmployees',
|
'taxID',
|
'vatID',
|
'duns',
|
'slogan',
|
'disambiguatingDescription',
|
]);
|
|
$this->type('LocalBusiness')
|
->label('Local Business')
|
->group('business')
|
->extends('Organization')
|
->addFields([
|
'location',
|
'openingHours',
|
'priceRange',
|
'currenciesAccepted',
|
'paymentAccepted',
|
'serviceArea',
|
'areaServed',
|
'hasMap',
|
'amenityFeature',
|
'availableLanguage',
|
'hasOfferCatalog',
|
'makesOffer',
|
'hasMenu',
|
'knowsAbout',
|
'hasCredential',
|
'aggregateRating',
|
'review',
|
'award',
|
]);
|
|
$this->type('TattooParlor')
|
->label('Tattoo Parlor')
|
->group('business')
|
->extends('LocalBusiness')
|
->addFields([
|
'makesOffer',
|
'hasOfferCatalog',
|
'award',
|
]);
|
|
$this->type('HealthBusiness')
|
->label('Health Business')
|
->group('business')
|
->extends('LocalBusiness');
|
|
$this->type('FoodEstablishment')
|
->label('Food Establishment')
|
->group('business')
|
->extends('LocalBusiness')
|
->addFields([
|
'hasMenu',
|
'servesCuisine',
|
]);
|
|
$this->type('FoodTruck')
|
->label('Food Truck')
|
->group('business')
|
->extends('FoodEstablishment')
|
->addField('serviceArea');
|
|
$this->type('Store')
|
->label('Store / Shop')
|
->group('business')
|
->extends('LocalBusiness')
|
->addFields([
|
'hasOfferCatalog',
|
'makesOffer',
|
]);
|
|
$this->type('ProfessionalService')
|
->label('Professional Service')
|
->group('business')
|
->extends('LocalBusiness')
|
->addFields([
|
'serviceArea',
|
'makesOffer',
|
'award',
|
]);
|
|
/**************************************************************
|
* PERSON
|
**************************************************************/
|
$this->type('Person')
|
->label('Person')
|
->group('person')
|
->fields([
|
'type',
|
'name',
|
'givenName',
|
'familyName',
|
'honorificPrefix',
|
'honorificSuffix',
|
'alternateName',
|
'description',
|
'image',
|
'url',
|
'email',
|
'telephone',
|
'sameAs',
|
'jobTitle',
|
'knowsLanguage',
|
'knowsAbout',
|
'award',
|
'hasCredential',
|
'birthDate',
|
'gender',
|
]);
|
|
/**************************************************************
|
* CREATIVE WORKS
|
**************************************************************/
|
$this->type('CreativeWork')
|
->label('Creative Work')
|
->group('creative')
|
->fields([
|
'type',
|
'name',
|
'description',
|
'image',
|
'author',
|
'creator',
|
'dateCreated',
|
'datePublished',
|
'dateModified',
|
'keywords',
|
'aggregateRating'
|
]);
|
|
$this->type('DefinedTerm')
|
->label('Defined Term')
|
->group('creative')
|
->extends('CreativeWork')
|
->addFields([
|
'termCode',
|
// 'inDefinedTermSet',
|
]);
|
|
$this->type('BeforeAfter')
|
->label('Before & After Case')
|
->group('creative')
|
->extends('CreativeWork')
|
->addFields([
|
'about',
|
'temporalCoverage',
|
'hasPart',
|
'associatedMedia',
|
'additionalProperty',
|
]);
|
|
$this->type('VisualArtwork')
|
->label('Visual Artwork')
|
->group('creative')
|
->extends('CreativeWork')
|
->addFields([
|
'artform',
|
'artMedium',
|
'artworkSurface',
|
'width',
|
'height',
|
]);
|
|
$this->type('Tattoo')
|
->label('Tattoo')
|
->group('creative')
|
->extends('VisualArtwork');
|
|
$this->type('Product')
|
->label('Product')
|
->group('creative')
|
->fields([
|
'name',
|
'description',
|
'image',
|
'brand',
|
'sku',
|
'gtin',
|
'offers',
|
'aggregateRating',
|
'review',
|
'award',
|
]);
|
|
/**************************************************************
|
* EVENTS
|
**************************************************************/
|
$this->type('Event')
|
->label('Event')
|
->group('event')
|
->fields([
|
'type',
|
'name',
|
'description',
|
'image',
|
'startDate',
|
'endDate',
|
'location',
|
'eventStatus',
|
'eventAttendanceMode',
|
]);
|
}
|
|
/**
|
* Define type groups for organization
|
*/
|
private function registerTypeGroups(): void
|
{
|
$this->typeGroups = [
|
'general' => 'General',
|
'page' => 'Page Types',
|
'business' => 'Business & Organization',
|
'person' => 'People',
|
'creative' => 'Creative Works',
|
'event' => 'Events',
|
];
|
}
|
}
|